From 908964c61715912933e48c501955364891991ee3 Mon Sep 17 00:00:00 2001 From: HarshPopat23 Date: Sun, 25 Oct 2026 09:44:40 +0530 Subject: [PATCH 1/6] Benchmark MCP protocol helpers Add gated MCP benchmarks for parsed metadata, separate JSON parsing, header mirroring, result writers and subscription intersection. Include Release-active fixture controls and reproducible measurement instructions. Signed-off-by: HarshPopat23 --- benchmark/CMakeLists.txt | 9 ++ benchmark/MCP.markdown | 76 ++++++++++++ benchmark/mcp.cc | 252 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 benchmark/MCP.markdown create mode 100644 benchmark/mcp.cc diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index b4aedefec..655cfe1d0 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -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() @@ -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) diff --git a/benchmark/MCP.markdown b/benchmark/MCP.markdown new file mode 100644 index 000000000..f07df6404 --- /dev/null +++ b/benchmark/MCP.markdown @@ -0,0 +1,76 @@ +# MCP performance baselines + +The `MCP_` cases use the existing Core benchmark harness. They measure local +protocol helpers, not model generation, networking, a transport/session runtime, +or application JSON Schema evaluation. No production code is optimized here. +Fixtures are deterministic, authored microbenchmark inputs, not captured traffic. + +## Run and record + +Configure a Release build with MCP and benchmarks enabled: + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + -DSOURCEMETA_CORE_MCP=ON -DSOURCEMETA_CORE_BENCHMARK=ON \ + -DSOURCEMETA_CORE_USE_SYSTEM_ALLOCATOR=ON +cmake --build build --config Release --target sourcemeta_core_benchmarks +# Single-configuration generators; multi-configuration builds may add Release/ +./build/benchmark/sourcemeta_core_benchmarks --filter MCP_ --output build/mcp.json +``` + +Keep the JSON output alongside the commit SHA, complete configure command, +`cmake -LA -N build` output, compiler/version (`c++ --version` or the platform +compiler equivalent), CPU/OS, build configuration, SIMD flags, and system load. +The command above explicitly selects the system allocator. Repeat in a separate +build with `SOURCEMETA_CORE_USE_SYSTEM_ALLOCATOR=OFF` to study the configured +mimalloc allocator; do not mix allocator/build/compiler results. The harness +JSON records timings and core count, not all of this environment information. + +Run the filtered executable multiple times on an otherwise idle machine. The +harness reports per-operation real/CPU time, a cold measurement, and the fastest +of three steady-state measurement windows; it does **not** report request p95 or +allocation counts. Use an external allocation profiler separately if needed, +and do not compare profiler timings with uninstrumented runs. There are no new +noisy timing thresholds in CI. MCP-disabled builds do not register these cases. + +## Workloads and measurement boundaries + +All fixture construction and preflight correctness checks occur before the +state loop. Checks throw on failure even with `NDEBUG`. Results are consumed +through the harness optimization barrier. Per-iteration output destruction is +included. Counts below are part of the benchmark names. + +| Cases | Small / large | Timed operation | +|---|---|---| +| `MCP_Parse_Metadata_*` | 0 / 128 extra metadata keys | Parse cached UTF-8 JSON and destroy the DOM; no MCP validation | +| `MCP_Validate_Metadata_*` | 0 / 128 extra keys | Validate an already parsed request, including metadata-key checks | +| `MCP_Reject_Metadata_*` | 0 / 128 extra keys | Reject object-valued `progressToken`, after other metadata checks | +| `MCP_Make_Headers_*` | 2 / 64 descriptors | Descriptor checks, header creation and plain/Unicode sentinel encoding | +| `MCP_Validate_Headers_*` | 2 / 64 descriptors | Validate already parsed arguments against 4 / 128 borrowed headers | +| `MCP_Reject_Headers_*` | 2 / 64 descriptors | Same, with a mismatch on the last mirrored parameter; includes error construction | +| `MCP_Write_Result_*` | 1 / 256 resources | Const-input result validation, copying, decoration, JSON-RPC envelope and ostream serialization | +| `MCP_Copy_Move_Write_Result_*` | 1 / 256 resources | Explicit fresh-payload copy plus move-taking writer; not a copy-free move comparison | +| `MCP_Reject_Result_*` | 1 / 256 resources | Invalid URI type on the last entry; includes exception construction/catching, not successful serialization | +| `MCP_Intersect_Overlap_*` | 8 / 512 URIs per side | Intersection with half overlap, preserving requested ordering | +| `MCP_Intersect_Disjoint_*` | 8 / 512 per side | Empty intersection; a valid no-match result, not malformed input | + +Each serialization iteration creates and destroys an `ostringstream`; its buffer +allocation is included. The successful const path is round-trip checked before +timing. Both writer variants receive the same source payload. The move-taking +path explicitly includes the fresh input copy required for repeated iterations. +These are whole public-API operations, not isolated JSON stringify kernels. + +The valid metadata wire sizes are **185 / 10,187 bytes** (compact UTF-8 JSON). +Each extra key is `bench-{index}` with a 64-byte ASCII value. Rejection adds +`progressToken:{}`. Resource payloads before envelope/cache decoration are +**201 / 48,435 bytes**; each resource has a name, a `file:///bench/{index}` URI +and a 128-byte description. Headers mirror alternating `plain` and ` café ` +values; half require UTF-8/base64 sentinel encoding. An equal number of unrelated +headers precedes mirrored headers to exercise lookup work. These sizes exclude +HTTP framing and are not protocol payload limits. + +Subscription URIs are `file:///bench/{index}`. Requested IDs are `[0,N)`; +supported IDs are `[N/2,3N/2)` for overlap and `[N,2N)` for disjoint workloads. +Owners are fully populated before any borrowed views are constructed. Reuse +these exact fixture shapes for before/after comparisons; do not attribute gains +from changing payloads, validation policy or included setup to an optimization. diff --git a/benchmark/mcp.cc b/benchmark/mcp.cc new file mode 100644 index 000000000..06ce10f68 --- /dev/null +++ b/benchmark/mcp.cc @@ -0,0 +1,252 @@ +#include +#include +#include + +#include // std::size_t +#include // std::ostringstream +#include // std::runtime_error +#include // std::string, std::to_string +#include // std::pair, std::move +#include // std::vector + +namespace { +using namespace sourcemeta::core; +constexpr auto VERSION{MCPProtocolVersion::V_2026_07_28}; + +// Unlike assert, preflight controls must also run in Release builds. All setup +// and controls precede the state loop, so they are outside its timing window. +auto check(const bool condition) -> void { + if (!condition) { + throw std::runtime_error{"Invalid MCP benchmark fixture"}; + } +} + +auto metadata(const std::size_t count, const bool invalid) -> JSON { + auto value{parse_json(R"JSON({"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28", + "io.modelcontextprotocol/clientCapabilities":{}},"name":"bench"}})JSON")}; + auto &meta{value.at("params").at("_meta")}; + for (std::size_t index{0}; index < count; ++index) { + meta.assign("bench-" + std::to_string(index), JSON{std::string(64, 'x')}); + } + if (invalid) { + meta.assign("progressToken", JSON::make_object()); + } + return value; +} + +auto validate_metadata(BenchmarkState &state, const std::size_t count, + const bool invalid) -> void { + const auto input{metadata(count, invalid)}; + const auto expected{invalid ? MCPRequestMetaStatus::InvalidProgressToken + : MCPRequestMetaStatus::Valid}; + check(mcp_validate_request_meta(input).first == expected); + for (auto iteration : state) { + const auto result{mcp_validate_request_meta(input)}; + benchmark_do_not_optimize(result); + } +} + +auto parse_metadata(BenchmarkState &state, const std::size_t count) -> void { + const auto input{metadata(count, false)}; + std::ostringstream encoded; + stringify(input, encoded); + const auto wire{encoded.str()}; + check(wire.size() == (count == 0 ? 185 : 10187)); + check(parse_json(wire) == input); + for (auto iteration : state) { + const auto result{parse_json(wire)}; + benchmark_do_not_optimize(result); + } +} + +// Owning strings are fully populated before constructing views. No owner is +// mutated after its views have been handed to descriptors/headers. +struct HeaderFixture { + std::vector names; + std::vector descriptors; + JSON arguments{JSON::make_object()}; + std::vector> owned; + std::vector> headers; + + explicit HeaderFixture(const std::size_t count, const bool invalid) { + 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é "}); + } + for (const auto &name : names) { + descriptors.push_back({name, {name}, JSON::Type::String}); + } + // Unrelated headers force the validator to search, not just match the + // first element. The invalid case fails at the last mirrored parameter. + for (std::size_t index{0}; index < count; ++index) { + owned.emplace_back("Other-" + std::to_string(index), "ignored"); + } + auto mirrored{mcp_make_parameter_headers(descriptors, arguments)}; + for (auto &entry : mirrored) { + owned.push_back(std::move(entry)); + } + if (invalid) { + owned.back().second = "mismatch"; + } + for (const auto &entry : owned) { + headers.emplace_back(entry.first, entry.second); + } + } +}; + +auto validate_headers(BenchmarkState &state, const std::size_t count, + const bool invalid) -> void { + const HeaderFixture input{count, invalid}; + const JSON identifier{1}; + check(mcp_validate_parameter_headers(VERSION, identifier, input.descriptors, + input.arguments, input.headers) + .has_value() == invalid); + for (auto iteration : state) { + const auto result{ + mcp_validate_parameter_headers(VERSION, identifier, input.descriptors, + input.arguments, input.headers)}; + benchmark_do_not_optimize(result); + } +} + +auto make_headers(BenchmarkState &state, const std::size_t count) -> void { + const HeaderFixture input{count, false}; + check(mcp_make_parameter_headers(input.descriptors, input.arguments).size() == + count); + for (auto iteration : state) { + const auto result{ + mcp_make_parameter_headers(input.descriptors, input.arguments)}; + benchmark_do_not_optimize(result); + } +} + +auto result_payload(const std::size_t count) -> JSON { + auto entries{JSON::make_array()}; + for (std::size_t index{0}; index < count; ++index) { + auto entry{JSON::make_object()}; + entry.assign("name", JSON{"item" + std::to_string(index)}); + entry.assign("uri", JSON{"file:///bench/" + std::to_string(index)}); + entry.assign("description", JSON{std::string(128, 'x')}); + entries.push_back(std::move(entry)); + } + auto result{JSON::make_object()}; + result.assign("resources", std::move(entries)); + return result; +} + +auto serialize_result(BenchmarkState &state, const std::size_t count, + const bool move, const bool invalid) -> void { + auto input{result_payload(count)}; + if (invalid) { + input.at("resources").at(count - 1).assign("uri", JSON{false}); + } + const JSON identifier{1}; + const MCPCachePolicy policy{.ttl_ms = 0, .scope = MCPCacheScope::Private}; + std::ostringstream control; + bool rejected{false}; + try { + mcp_write_result(control, VERSION, "resources/list", identifier, input, + policy); + } catch (const MCPError &) { + rejected = true; + } + check(rejected == invalid); + if (!invalid) { + std::ostringstream payload; + stringify(input, payload); + check(payload.str().size() == (count == 1 ? 201 : 48435)); + const auto output{parse_json(control.str())}; + check(output.at("result").at("resources") == input.at("resources")); + if (move) { + auto fresh{input}; + std::ostringstream moved; + mcp_write_result(moved, VERSION, "resources/list", identifier, + std::move(fresh), policy); + check(moved.str() == control.str()); + } + } + for (auto iteration : state) { + std::ostringstream stream; + if (invalid) { + try { + mcp_write_result(stream, VERSION, "resources/list", identifier, input, + policy); + } catch (const MCPError &error) { + benchmark_do_not_optimize(error); + } + } else if (move) { + // Each repetition needs an intact payload. Include this copy explicitly; + // this is copy + move-write, not a fictitious copy-free move comparison. + auto fresh{input}; + mcp_write_result(stream, VERSION, "resources/list", identifier, + std::move(fresh), policy); + } else { + mcp_write_result(stream, VERSION, "resources/list", identifier, input, + policy); + } + benchmark_do_not_optimize(stream); + } +} + +auto intersect(BenchmarkState &state, const std::size_t count, + const bool overlap) -> void { + std::vector requested; + std::vector supported; + for (std::size_t index{0}; index < count; ++index) { + requested.push_back("file:///bench/" + std::to_string(index)); + supported.push_back("file:///bench/" + + std::to_string(index + (overlap ? count / 2 : count))); + } + 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); + } + const auto control{mcp_intersect_subscription_filters(left, right)}; + check(control.resource_subscriptions && + control.resource_subscriptions->size() == (overlap ? count / 2 : 0)); + if (overlap) { + for (std::size_t index{0}; index < count / 2; ++index) { + check(control.resource_subscriptions->at(index) == + requested.at(index + count / 2)); + } + } + for (auto iteration : state) { + const auto result{mcp_intersect_subscription_filters(left, right)}; + benchmark_do_not_optimize(result); + } +} +} // namespace + +BENCHMARK(MCP_Parse_Metadata_0) { parse_metadata(state, 0); } +BENCHMARK(MCP_Parse_Metadata_128) { parse_metadata(state, 128); } +BENCHMARK(MCP_Validate_Metadata_0) { validate_metadata(state, 0, false); } +BENCHMARK(MCP_Validate_Metadata_128) { validate_metadata(state, 128, false); } +BENCHMARK(MCP_Reject_Metadata_0) { validate_metadata(state, 0, true); } +BENCHMARK(MCP_Reject_Metadata_128) { validate_metadata(state, 128, true); } +BENCHMARK(MCP_Validate_Headers_2) { validate_headers(state, 2, false); } +BENCHMARK(MCP_Validate_Headers_64) { validate_headers(state, 64, false); } +BENCHMARK(MCP_Reject_Headers_2) { validate_headers(state, 2, true); } +BENCHMARK(MCP_Reject_Headers_64) { validate_headers(state, 64, true); } +BENCHMARK(MCP_Make_Headers_2) { make_headers(state, 2); } +BENCHMARK(MCP_Make_Headers_64) { make_headers(state, 64); } +BENCHMARK(MCP_Write_Result_1) { serialize_result(state, 1, false, false); } +BENCHMARK(MCP_Write_Result_256) { serialize_result(state, 256, false, false); } +BENCHMARK(MCP_Copy_Move_Write_Result_1) { + serialize_result(state, 1, true, false); +} +BENCHMARK(MCP_Copy_Move_Write_Result_256) { + serialize_result(state, 256, true, false); +} +BENCHMARK(MCP_Reject_Result_1) { serialize_result(state, 1, false, true); } +BENCHMARK(MCP_Reject_Result_256) { serialize_result(state, 256, false, true); } +BENCHMARK(MCP_Intersect_Overlap_8) { intersect(state, 8, true); } +BENCHMARK(MCP_Intersect_Overlap_512) { intersect(state, 512, true); } +BENCHMARK(MCP_Intersect_Disjoint_8) { intersect(state, 8, false); } +BENCHMARK(MCP_Intersect_Disjoint_512) { intersect(state, 512, false); } From 2b04fb02451ba10d7492b71f244f12487b71fd69 Mon Sep 17 00:00:00 2001 From: HarshPopat23 Date: Sun, 25 Oct 2026 09:57:31 +0530 Subject: [PATCH 2/6] Optimize MCP parameter-header validation and lookup Borrow descriptor names, avoid small-list set allocations and index larger mirrored-header collections. Preserve duplicate and exact numeric behavior. Add indexed-path regression controls and scoped comparative measurements. Signed-off-by: HarshPopat23 --- benchmark/MCP-header-results.markdown | 22 ++++++ src/core/mcp/parameter_headers.cc | 96 +++++++++++++++++++++------ test/mcp/mcp_headers_test.cc | 61 +++++++++++++++++ 3 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 benchmark/MCP-header-results.markdown diff --git a/benchmark/MCP-header-results.markdown b/benchmark/MCP-header-results.markdown new file mode 100644 index 000000000..0b9c6baa2 --- /dev/null +++ b/benchmark/MCP-header-results.markdown @@ -0,0 +1,22 @@ +# Parameter-header optimization measurements + +Dependent on the MCP baselines in #3031. Public signatures and validation policy are unchanged. + +Local Linux shared runner, GCC 13.3, CMake Release (including configured LTO/SIMD), system allocator. Identical PR 1 fixtures; three alternating baseline/candidate process runs. Values are the median across runs of the harness realTime statistic (fastest of three measurement windows), not per-request p95. No compilation ran during the retained comparisons. An earlier executable-relink collision failed to run the candidate and was discarded. + +| Case | Baseline median ns/op | Candidate median ns/op | Change | +|---|---:|---:|---:| +| MCP_Validate_Headers_2 | 418.99 | 222.58 | -46.88% | +| MCP_Validate_Headers_64 | 69469.24 | 24605.64 | -64.58% | +| MCP_Reject_Headers_2 | 1433.99 | 1235.73 | -13.83% | +| MCP_Reject_Headers_64 | 69548.79 | 26157.13 | -62.39% | +| MCP_Make_Headers_2 | 496.96 | 396.24 | -20.27% | +| MCP_Make_Headers_64 | 23456.31 | 23939.87 | +2.06% | + +Small descriptor lists (up to eight) use a borrowed-name duplicate scan without set-node allocations. Larger lists retain ordered-set checking with borrowed views and an ASCII case-insensitive comparator; descriptors are still validated each call. No cached-descriptor API or lifetime change is introduced. + +Header validation avoids constructing prefixed strings on successful paths. For more than eight descriptors and sixteen supplied headers it builds one sorted borrowed-view index of mirrored headers, then uses binary lookup. Small inputs retain a linear scan. Duplicate matching headers still fail at the relevant parameter; duplicate unused headers remain opaque. Error paths still construct the same full header names. Decoding and exact numeric validation remain unchanged. + +The thresholds are internal heuristics, not a maximum count or a proven optimal crossover. Large generation was 2.06% slower in this sample; no broad non-regression or end-to-end speed claim is made. These runs cover 2/64 descriptors and short names; other cardinalities, name lengths, compilers and allocators require further measurement. + +Reproduce with the instructions in MCP.markdown. Build the PR 1 parent and this branch with identical settings in separate directories, alternate `--filter Headers_ --output before-N.json` and `--filter Headers_ --output after-N.json` for three runs, and compare each identically named case. Keep all output/environment records; do not select only favorable cases. diff --git a/src/core/mcp/parameter_headers.cc b/src/core/mcp/parameter_headers.cc index 877989dfa..d8b7d1e7c 100644 --- a/src/core/mcp/parameter_headers.cc +++ b/src/core/mcp/parameter_headers.cc @@ -7,9 +7,11 @@ #include #include -#include // std::set -#include // std::string, std::to_string -#include // std::vector +#include // std::sort, std::lower_bound, std::lexicographical_compare +#include // std::next +#include // std::set +#include // std::string, std::to_string +#include // std::vector namespace { using sourcemeta::core::JSON; @@ -33,12 +35,23 @@ auto value_at(const JSON &arguments, const MCPHeaderParameter ¶meter) // 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 a, const char b) { + return sourcemeta::core::to_lowercase(a) < + sourcemeta::core::to_lowercase(b); + }); + } +}; + auto require_parameters(const std::span parameters) -> void { - std::set names; + std::set names; + std::size_t index{0}; for (const auto ¶meter : 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( @@ -55,9 +68,21 @@ auto require_parameters(const std::span 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"); } } @@ -141,24 +166,57 @@ 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> 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, "Mcp-Param-")) { + indexed.emplace_back(entry.first.substr(10), entry.second); + } + } + std::sort(indexed.begin(), indexed.end(), + [](const auto &left, const auto &right) { + return NameLess{}(left.first, right.first); + }); + } for (const auto ¶meter : parameters) { - const std::string name{"Mcp-Param-" + std::string{parameter.name}}; - std::optional 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, "Mcp-Param-") && + equals_ignore_case(candidate.first.substr(10), 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, "Mcp-Param-" + std::string{parameter.name}, + *header); } continue; } @@ -184,9 +242,9 @@ 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, "Mcp-Param-" + std::string{parameter.name}, + header.value_or(""), parameter_text(*value)); } } return std::nullopt; diff --git a/test/mcp/mcp_headers_test.cc b/test/mcp/mcp_headers_test.cc index 6c435e42d..ca7e774a8 100644 --- a/test/mcp/mcp_headers_test.cc +++ b/test/mcp/mcp_headers_test.cc @@ -4041,3 +4041,64 @@ TEST(parameter_integer_rejects_a_header_that_is_not_a_number) { sourcemeta::core::JSON{0}, descriptors, arguments, views) .has_value()); } + +TEST(parameter_headers_index_preserves_case_duplicates_and_encoding) { + using namespace sourcemeta::core; + std::vector names; + auto arguments{JSON::make_object()}; + for (std::size_t index{0}; index < 9; ++index) { + names.push_back("field" + std::to_string(index)); + arguments.assign(names.back(), JSON{" café "}); + } + std::vector parameters; + for (const auto &name : names) { + parameters.push_back({name, {name}, JSON::Type::String}); + } + auto owned{mcp_make_parameter_headers(parameters, arguments)}; + std::vector> headers; + for (std::size_t index{0}; index < 9; ++index) { + owned[index].first.replace(0, 10, "mCp-pArAm-"); + } + for (auto entry{owned.rbegin()}; entry != owned.rend(); ++entry) { + headers.emplace_back(entry->first, entry->second); + } + // Force the indexed path; duplicate unused names must stay opaque. + for (std::size_t index{0}; index < 9; ++index) { + headers.emplace_back("Mcp-Param-unused", "ignored"); + } + const auto version{MCPProtocolVersion::V_2026_07_28}; + const JSON identifier{1}; + EXPECT_FALSE(mcp_validate_parameter_headers(version, identifier, parameters, + arguments, headers) + .has_value()); + headers.push_back(headers.front()); + EXPECT_TRUE(mcp_validate_parameter_headers(version, identifier, parameters, + arguments, headers) + .has_value()); + headers.pop_back(); + arguments.assign("field8", JSON{true}); + EXPECT_TRUE(mcp_validate_parameter_headers(version, identifier, parameters, + arguments, headers) + .has_value()); + parameters.back().type = JSON::Type::Integer; + arguments.assign("field8", parse_json("9007199254740991.0")); + headers.clear(); + owned = mcp_make_parameter_headers(parameters, arguments); + for (const auto &entry : owned) { + headers.emplace_back(entry.first, entry.second); + } + for (std::size_t index{0}; index < 9; ++index) { + headers.emplace_back("Other", "ignored"); + } + EXPECT_FALSE(mcp_validate_parameter_headers(version, identifier, parameters, + arguments, headers) + .has_value()); + arguments.assign("field8", JSON{INT64_C(9007199254740992)}); + EXPECT_TRUE(mcp_validate_parameter_headers(version, identifier, parameters, + arguments, headers) + .has_value()); + parameters.back().name = "FIELD0"; + EXPECT_MCP_REJECTS( + mcp_make_parameter_headers(parameters, arguments), + "Parameter header names must be case-insensitively unique"); +} From 81d356a80ce0bf61de173a5d48d1cb0e59acf408 Mon Sep 17 00:00:00 2001 From: HarshPopat23 Date: Sun, 25 Oct 2026 11:11:07 +0530 Subject: [PATCH 3/6] fix(ci): satisfy MCP comparator and benchmark clang-tidy checks Use descriptive comparator parameter names for readability-identifier-length. Carry the existing #3031 benchmark lint fixes onto this dependent branch: designated descriptor initialization and explicit arithmetic grouping. No validation or benchmark behavior changes. Signed-off-by: HarshPopat23 --- benchmark/mcp.cc | 5 +++-- src/core/mcp/parameter_headers.cc | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/benchmark/mcp.cc b/benchmark/mcp.cc index 06ce10f68..b2064ca4c 100644 --- a/benchmark/mcp.cc +++ b/benchmark/mcp.cc @@ -75,7 +75,8 @@ struct HeaderFixture { arguments.assign(names.back(), JSON{index % 2 == 0 ? "plain" : " café "}); } for (const auto &name : names) { - descriptors.push_back({name, {name}, JSON::Type::String}); + descriptors.push_back( + {.name = name, .path = {name}, .type = JSON::Type::String}); } // Unrelated headers force the validator to search, not just match the // first element. The invalid case fails at the last mirrored parameter. @@ -214,7 +215,7 @@ auto intersect(BenchmarkState &state, const std::size_t count, if (overlap) { for (std::size_t index{0}; index < count / 2; ++index) { check(control.resource_subscriptions->at(index) == - requested.at(index + count / 2)); + requested.at(index + (count / 2))); } } for (auto iteration : state) { diff --git a/src/core/mcp/parameter_headers.cc b/src/core/mcp/parameter_headers.cc index d8b7d1e7c..c473bb587 100644 --- a/src/core/mcp/parameter_headers.cc +++ b/src/core/mcp/parameter_headers.cc @@ -40,9 +40,9 @@ struct NameLess { const JSON::StringView right) const -> bool { return std::lexicographical_compare( left.begin(), left.end(), right.begin(), right.end(), - [](const char a, const char b) { - return sourcemeta::core::to_lowercase(a) < - sourcemeta::core::to_lowercase(b); + [](const char left_character, const char right_character) { + return sourcemeta::core::to_lowercase(left_character) < + sourcemeta::core::to_lowercase(right_character); }); } }; From 3ebb9855d701de9850219c8d61e949880d22888c Mon Sep 17 00:00:00 2001 From: HarshPopat23 Date: Sun, 25 Oct 2026 14:20:16 +0530 Subject: [PATCH 4/6] fix(test): satisfy clang-tidy for indexed MCP header fixture Reserve descriptor capacity and use named aggregate fields. Preserve all test inputs and assertions. Verify all changed C++ files with the full repository clang-tidy configuration and rerun the 1,421-test MCP suite. Signed-off-by: HarshPopat23 --- test/mcp/mcp_headers_test.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/mcp/mcp_headers_test.cc b/test/mcp/mcp_headers_test.cc index ca7e774a8..0ed4dc77c 100644 --- a/test/mcp/mcp_headers_test.cc +++ b/test/mcp/mcp_headers_test.cc @@ -4051,8 +4051,10 @@ TEST(parameter_headers_index_preserves_case_duplicates_and_encoding) { arguments.assign(names.back(), JSON{" café "}); } std::vector parameters; + parameters.reserve(names.size()); for (const auto &name : names) { - parameters.push_back({name, {name}, JSON::Type::String}); + parameters.push_back( + {.name = name, .path = {name}, .type = JSON::Type::String}); } auto owned{mcp_make_parameter_headers(parameters, arguments)}; std::vector> headers; From 0e320d0ce458de46ee3daa76a98da5df8b9e8fea Mon Sep 17 00:00:00 2001 From: HarshPopat23 Date: Sun, 25 Oct 2026 15:27:27 +0530 Subject: [PATCH 5/6] fix(build): load the static allocator before C++ runtime allocation symbols For Linux static mimalloc builds, apply WHOLE_ARCHIVE to every reference to the allocator target so empty/LTO executables cannot select libstdc++ new/delete before later extracting the allocator. Keep shared and system-allocator builds unchanged and preserve static runtime linking. Local GCC 13 Release checks: allocator tests pass, empty framework test retains its expected TAP output/exit code, and operator new resolves to mi_new. The reported collision occurs with CI's GCC 15 and requires CI confirmation. Signed-off-by: HarshPopat23 --- cmake/common/targets/executable.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmake/common/targets/executable.cmake b/cmake/common/targets/executable.cmake index d4d253136..b911dc5fd 100644 --- a/cmake/common/targets/executable.cmake +++ b/cmake/common/targets/executable.cmake @@ -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 all references to this + # target, including explicit allocator links on allocator test targets. + 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 From 0206b7fd520e40e205b57d2ea26d719df3e270ba Mon Sep 17 00:00:00 2001 From: HarshPopat23 Date: Tue, 27 Oct 2026 09:03:38 +0530 Subject: [PATCH 6/6] Address MCP parameter-header review feedback Keep the three focused benchmarks from #3031 and remove standalone notes. Split indexed-header regressions into independent cases, derive header prefix offsets from one constant, and clarify allocator override scope. Signed-off-by: HarshPopat23 --- benchmark/MCP-header-results.markdown | 22 --- benchmark/MCP.markdown | 76 -------- benchmark/mcp.cc | 246 +++++--------------------- cmake/common/targets/executable.cmake | 4 +- src/core/mcp/parameter_headers.cc | 21 ++- test/mcp/mcp_headers_test.cc | 174 +++++++++++------- 6 files changed, 171 insertions(+), 372 deletions(-) delete mode 100644 benchmark/MCP-header-results.markdown delete mode 100644 benchmark/MCP.markdown diff --git a/benchmark/MCP-header-results.markdown b/benchmark/MCP-header-results.markdown deleted file mode 100644 index 0b9c6baa2..000000000 --- a/benchmark/MCP-header-results.markdown +++ /dev/null @@ -1,22 +0,0 @@ -# Parameter-header optimization measurements - -Dependent on the MCP baselines in #3031. Public signatures and validation policy are unchanged. - -Local Linux shared runner, GCC 13.3, CMake Release (including configured LTO/SIMD), system allocator. Identical PR 1 fixtures; three alternating baseline/candidate process runs. Values are the median across runs of the harness realTime statistic (fastest of three measurement windows), not per-request p95. No compilation ran during the retained comparisons. An earlier executable-relink collision failed to run the candidate and was discarded. - -| Case | Baseline median ns/op | Candidate median ns/op | Change | -|---|---:|---:|---:| -| MCP_Validate_Headers_2 | 418.99 | 222.58 | -46.88% | -| MCP_Validate_Headers_64 | 69469.24 | 24605.64 | -64.58% | -| MCP_Reject_Headers_2 | 1433.99 | 1235.73 | -13.83% | -| MCP_Reject_Headers_64 | 69548.79 | 26157.13 | -62.39% | -| MCP_Make_Headers_2 | 496.96 | 396.24 | -20.27% | -| MCP_Make_Headers_64 | 23456.31 | 23939.87 | +2.06% | - -Small descriptor lists (up to eight) use a borrowed-name duplicate scan without set-node allocations. Larger lists retain ordered-set checking with borrowed views and an ASCII case-insensitive comparator; descriptors are still validated each call. No cached-descriptor API or lifetime change is introduced. - -Header validation avoids constructing prefixed strings on successful paths. For more than eight descriptors and sixteen supplied headers it builds one sorted borrowed-view index of mirrored headers, then uses binary lookup. Small inputs retain a linear scan. Duplicate matching headers still fail at the relevant parameter; duplicate unused headers remain opaque. Error paths still construct the same full header names. Decoding and exact numeric validation remain unchanged. - -The thresholds are internal heuristics, not a maximum count or a proven optimal crossover. Large generation was 2.06% slower in this sample; no broad non-regression or end-to-end speed claim is made. These runs cover 2/64 descriptors and short names; other cardinalities, name lengths, compilers and allocators require further measurement. - -Reproduce with the instructions in MCP.markdown. Build the PR 1 parent and this branch with identical settings in separate directories, alternate `--filter Headers_ --output before-N.json` and `--filter Headers_ --output after-N.json` for three runs, and compare each identically named case. Keep all output/environment records; do not select only favorable cases. diff --git a/benchmark/MCP.markdown b/benchmark/MCP.markdown deleted file mode 100644 index f07df6404..000000000 --- a/benchmark/MCP.markdown +++ /dev/null @@ -1,76 +0,0 @@ -# MCP performance baselines - -The `MCP_` cases use the existing Core benchmark harness. They measure local -protocol helpers, not model generation, networking, a transport/session runtime, -or application JSON Schema evaluation. No production code is optimized here. -Fixtures are deterministic, authored microbenchmark inputs, not captured traffic. - -## Run and record - -Configure a Release build with MCP and benchmarks enabled: - -```sh -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ - -DSOURCEMETA_CORE_MCP=ON -DSOURCEMETA_CORE_BENCHMARK=ON \ - -DSOURCEMETA_CORE_USE_SYSTEM_ALLOCATOR=ON -cmake --build build --config Release --target sourcemeta_core_benchmarks -# Single-configuration generators; multi-configuration builds may add Release/ -./build/benchmark/sourcemeta_core_benchmarks --filter MCP_ --output build/mcp.json -``` - -Keep the JSON output alongside the commit SHA, complete configure command, -`cmake -LA -N build` output, compiler/version (`c++ --version` or the platform -compiler equivalent), CPU/OS, build configuration, SIMD flags, and system load. -The command above explicitly selects the system allocator. Repeat in a separate -build with `SOURCEMETA_CORE_USE_SYSTEM_ALLOCATOR=OFF` to study the configured -mimalloc allocator; do not mix allocator/build/compiler results. The harness -JSON records timings and core count, not all of this environment information. - -Run the filtered executable multiple times on an otherwise idle machine. The -harness reports per-operation real/CPU time, a cold measurement, and the fastest -of three steady-state measurement windows; it does **not** report request p95 or -allocation counts. Use an external allocation profiler separately if needed, -and do not compare profiler timings with uninstrumented runs. There are no new -noisy timing thresholds in CI. MCP-disabled builds do not register these cases. - -## Workloads and measurement boundaries - -All fixture construction and preflight correctness checks occur before the -state loop. Checks throw on failure even with `NDEBUG`. Results are consumed -through the harness optimization barrier. Per-iteration output destruction is -included. Counts below are part of the benchmark names. - -| Cases | Small / large | Timed operation | -|---|---|---| -| `MCP_Parse_Metadata_*` | 0 / 128 extra metadata keys | Parse cached UTF-8 JSON and destroy the DOM; no MCP validation | -| `MCP_Validate_Metadata_*` | 0 / 128 extra keys | Validate an already parsed request, including metadata-key checks | -| `MCP_Reject_Metadata_*` | 0 / 128 extra keys | Reject object-valued `progressToken`, after other metadata checks | -| `MCP_Make_Headers_*` | 2 / 64 descriptors | Descriptor checks, header creation and plain/Unicode sentinel encoding | -| `MCP_Validate_Headers_*` | 2 / 64 descriptors | Validate already parsed arguments against 4 / 128 borrowed headers | -| `MCP_Reject_Headers_*` | 2 / 64 descriptors | Same, with a mismatch on the last mirrored parameter; includes error construction | -| `MCP_Write_Result_*` | 1 / 256 resources | Const-input result validation, copying, decoration, JSON-RPC envelope and ostream serialization | -| `MCP_Copy_Move_Write_Result_*` | 1 / 256 resources | Explicit fresh-payload copy plus move-taking writer; not a copy-free move comparison | -| `MCP_Reject_Result_*` | 1 / 256 resources | Invalid URI type on the last entry; includes exception construction/catching, not successful serialization | -| `MCP_Intersect_Overlap_*` | 8 / 512 URIs per side | Intersection with half overlap, preserving requested ordering | -| `MCP_Intersect_Disjoint_*` | 8 / 512 per side | Empty intersection; a valid no-match result, not malformed input | - -Each serialization iteration creates and destroys an `ostringstream`; its buffer -allocation is included. The successful const path is round-trip checked before -timing. Both writer variants receive the same source payload. The move-taking -path explicitly includes the fresh input copy required for repeated iterations. -These are whole public-API operations, not isolated JSON stringify kernels. - -The valid metadata wire sizes are **185 / 10,187 bytes** (compact UTF-8 JSON). -Each extra key is `bench-{index}` with a 64-byte ASCII value. Rejection adds -`progressToken:{}`. Resource payloads before envelope/cache decoration are -**201 / 48,435 bytes**; each resource has a name, a `file:///bench/{index}` URI -and a 128-byte description. Headers mirror alternating `plain` and ` café ` -values; half require UTF-8/base64 sentinel encoding. An equal number of unrelated -headers precedes mirrored headers to exercise lookup work. These sizes exclude -HTTP framing and are not protocol payload limits. - -Subscription URIs are `file:///bench/{index}`. Requested IDs are `[0,N)`; -supported IDs are `[N/2,3N/2)` for overlap and `[N,2N)` for disjoint workloads. -Owners are fully populated before any borrowed views are constructed. Reuse -these exact fixture shapes for before/after comparisons; do not attribute gains -from changing payloads, validation policy or included setup to an optimization. diff --git a/benchmark/mcp.cc b/benchmark/mcp.cc index b2064ca4c..6430f8f89 100644 --- a/benchmark/mcp.cc +++ b/benchmark/mcp.cc @@ -1,9 +1,7 @@ #include -#include #include #include // std::size_t -#include // std::ostringstream #include // std::runtime_error #include // std::string, std::to_string #include // std::pair, std::move @@ -11,194 +9,66 @@ namespace { using namespace sourcemeta::core; -constexpr auto VERSION{MCPProtocolVersion::V_2026_07_28}; -// Unlike assert, preflight controls must also run in Release builds. All setup -// and controls precede the state loop, so they are outside its timing window. -auto check(const bool condition) -> void { - if (!condition) { - throw std::runtime_error{"Invalid MCP benchmark fixture"}; - } -} - -auto metadata(const std::size_t count, const bool invalid) -> JSON { - auto value{parse_json(R"JSON({"jsonrpc":"2.0","id":1,"method":"tools/call", - "params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28", - "io.modelcontextprotocol/clientCapabilities":{}},"name":"bench"}})JSON")}; - auto &meta{value.at("params").at("_meta")}; +auto validate_headers(BenchmarkState &state, const std::size_t count) -> void { + std::vector names; + names.reserve(count); + auto arguments{JSON::make_object()}; for (std::size_t index{0}; index < count; ++index) { - meta.assign("bench-" + std::to_string(index), JSON{std::string(64, 'x')}); - } - if (invalid) { - meta.assign("progressToken", JSON::make_object()); + names.push_back("field" + std::to_string(index)); + arguments.assign(names.back(), JSON{index % 2 == 0 ? "plain" : " café "}); } - return value; -} -auto validate_metadata(BenchmarkState &state, const std::size_t count, - const bool invalid) -> void { - const auto input{metadata(count, invalid)}; - const auto expected{invalid ? MCPRequestMetaStatus::InvalidProgressToken - : MCPRequestMetaStatus::Valid}; - check(mcp_validate_request_meta(input).first == expected); - for (auto iteration : state) { - const auto result{mcp_validate_request_meta(input)}; - benchmark_do_not_optimize(result); + std::vector parameters; + parameters.reserve(count); + for (const auto &name : names) { + parameters.push_back( + {.name = name, .path = {name}, .type = JSON::Type::String}); } -} -auto parse_metadata(BenchmarkState &state, const std::size_t count) -> void { - const auto input{metadata(count, false)}; - std::ostringstream encoded; - stringify(input, encoded); - const auto wire{encoded.str()}; - check(wire.size() == (count == 0 ? 185 : 10187)); - check(parse_json(wire) == input); - for (auto iteration : state) { - const auto result{parse_json(wire)}; - benchmark_do_not_optimize(result); - } -} - -// Owning strings are fully populated before constructing views. No owner is -// mutated after its views have been handed to descriptors/headers. -struct HeaderFixture { - std::vector names; - std::vector descriptors; - JSON arguments{JSON::make_object()}; + // Put unrelated headers first to exercise the per-parameter linear search. std::vector> 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> headers; - - explicit HeaderFixture(const std::size_t count, const bool invalid) { - 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é "}); - } - for (const auto &name : names) { - descriptors.push_back( - {.name = name, .path = {name}, .type = JSON::Type::String}); - } - // Unrelated headers force the validator to search, not just match the - // first element. The invalid case fails at the last mirrored parameter. - for (std::size_t index{0}; index < count; ++index) { - owned.emplace_back("Other-" + std::to_string(index), "ignored"); - } - auto mirrored{mcp_make_parameter_headers(descriptors, arguments)}; - for (auto &entry : mirrored) { - owned.push_back(std::move(entry)); - } - if (invalid) { - owned.back().second = "mismatch"; - } - for (const auto &entry : owned) { - headers.emplace_back(entry.first, entry.second); - } + headers.reserve(owned.size()); + for (const auto &header : owned) { + headers.emplace_back(header.first, header.second); } -}; -auto validate_headers(BenchmarkState &state, const std::size_t count, - const bool invalid) -> void { - const HeaderFixture input{count, invalid}; + constexpr auto VERSION{MCPProtocolVersion::V_2026_07_28}; const JSON identifier{1}; - check(mcp_validate_parameter_headers(VERSION, identifier, input.descriptors, - input.arguments, input.headers) - .has_value() == invalid); - for (auto iteration : state) { - const auto result{ - mcp_validate_parameter_headers(VERSION, identifier, input.descriptors, - input.arguments, input.headers)}; - benchmark_do_not_optimize(result); + if (mcp_validate_parameter_headers(VERSION, identifier, parameters, arguments, + headers)) { + throw std::runtime_error{"Invalid MCP header benchmark fixture"}; } -} - -auto make_headers(BenchmarkState &state, const std::size_t count) -> void { - const HeaderFixture input{count, false}; - check(mcp_make_parameter_headers(input.descriptors, input.arguments).size() == - count); for (auto iteration : state) { - const auto result{ - mcp_make_parameter_headers(input.descriptors, input.arguments)}; + const auto result{mcp_validate_parameter_headers( + VERSION, identifier, parameters, arguments, headers)}; benchmark_do_not_optimize(result); } } +} // namespace -auto result_payload(const std::size_t count) -> JSON { - auto entries{JSON::make_array()}; - for (std::size_t index{0}; index < count; ++index) { - auto entry{JSON::make_object()}; - entry.assign("name", JSON{"item" + std::to_string(index)}); - entry.assign("uri", JSON{"file:///bench/" + std::to_string(index)}); - entry.assign("description", JSON{std::string(128, 'x')}); - entries.push_back(std::move(entry)); - } - auto result{JSON::make_object()}; - result.assign("resources", std::move(entries)); - return result; -} +BENCHMARK(MCP_Validate_Headers_2) { validate_headers(state, 2); } +BENCHMARK(MCP_Validate_Headers_64) { validate_headers(state, 64); } -auto serialize_result(BenchmarkState &state, const std::size_t count, - const bool move, const bool invalid) -> void { - auto input{result_payload(count)}; - if (invalid) { - input.at("resources").at(count - 1).assign("uri", JSON{false}); - } - const JSON identifier{1}; - const MCPCachePolicy policy{.ttl_ms = 0, .scope = MCPCacheScope::Private}; - std::ostringstream control; - bool rejected{false}; - try { - mcp_write_result(control, VERSION, "resources/list", identifier, input, - policy); - } catch (const MCPError &) { - rejected = true; - } - check(rejected == invalid); - if (!invalid) { - std::ostringstream payload; - stringify(input, payload); - check(payload.str().size() == (count == 1 ? 201 : 48435)); - const auto output{parse_json(control.str())}; - check(output.at("result").at("resources") == input.at("resources")); - if (move) { - auto fresh{input}; - std::ostringstream moved; - mcp_write_result(moved, VERSION, "resources/list", identifier, - std::move(fresh), policy); - check(moved.str() == control.str()); - } - } - for (auto iteration : state) { - std::ostringstream stream; - if (invalid) { - try { - mcp_write_result(stream, VERSION, "resources/list", identifier, input, - policy); - } catch (const MCPError &error) { - benchmark_do_not_optimize(error); - } - } else if (move) { - // Each repetition needs an intact payload. Include this copy explicitly; - // this is copy + move-write, not a fictitious copy-free move comparison. - auto fresh{input}; - mcp_write_result(stream, VERSION, "resources/list", identifier, - std::move(fresh), policy); - } else { - mcp_write_result(stream, VERSION, "resources/list", identifier, input, - policy); - } - benchmark_do_not_optimize(stream); - } -} - -auto intersect(BenchmarkState &state, const std::size_t count, - const bool overlap) -> void { +BENCHMARK(MCP_Intersect_Disjoint_512) { + using namespace sourcemeta::core; std::vector requested; std::vector supported; - for (std::size_t index{0}; index < count; ++index) { + 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 + (overlap ? count / 2 : count))); + supported.push_back("file:///bench/" + std::to_string(index + 512)); } + MCPSubscriptionFilter left; MCPSubscriptionFilter right; left.resource_subscriptions.emplace(); @@ -209,45 +79,15 @@ auto intersect(BenchmarkState &state, const std::size_t count, 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)}; - check(control.resource_subscriptions && - control.resource_subscriptions->size() == (overlap ? count / 2 : 0)); - if (overlap) { - for (std::size_t index{0}; index < count / 2; ++index) { - check(control.resource_subscriptions->at(index) == - requested.at(index + (count / 2))); - } + 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); } } -} // namespace - -BENCHMARK(MCP_Parse_Metadata_0) { parse_metadata(state, 0); } -BENCHMARK(MCP_Parse_Metadata_128) { parse_metadata(state, 128); } -BENCHMARK(MCP_Validate_Metadata_0) { validate_metadata(state, 0, false); } -BENCHMARK(MCP_Validate_Metadata_128) { validate_metadata(state, 128, false); } -BENCHMARK(MCP_Reject_Metadata_0) { validate_metadata(state, 0, true); } -BENCHMARK(MCP_Reject_Metadata_128) { validate_metadata(state, 128, true); } -BENCHMARK(MCP_Validate_Headers_2) { validate_headers(state, 2, false); } -BENCHMARK(MCP_Validate_Headers_64) { validate_headers(state, 64, false); } -BENCHMARK(MCP_Reject_Headers_2) { validate_headers(state, 2, true); } -BENCHMARK(MCP_Reject_Headers_64) { validate_headers(state, 64, true); } -BENCHMARK(MCP_Make_Headers_2) { make_headers(state, 2); } -BENCHMARK(MCP_Make_Headers_64) { make_headers(state, 64); } -BENCHMARK(MCP_Write_Result_1) { serialize_result(state, 1, false, false); } -BENCHMARK(MCP_Write_Result_256) { serialize_result(state, 256, false, false); } -BENCHMARK(MCP_Copy_Move_Write_Result_1) { - serialize_result(state, 1, true, false); -} -BENCHMARK(MCP_Copy_Move_Write_Result_256) { - serialize_result(state, 256, true, false); -} -BENCHMARK(MCP_Reject_Result_1) { serialize_result(state, 1, false, true); } -BENCHMARK(MCP_Reject_Result_256) { serialize_result(state, 256, false, true); } -BENCHMARK(MCP_Intersect_Overlap_8) { intersect(state, 8, true); } -BENCHMARK(MCP_Intersect_Overlap_512) { intersect(state, 512, true); } -BENCHMARK(MCP_Intersect_Disjoint_8) { intersect(state, 8, false); } -BENCHMARK(MCP_Intersect_Disjoint_512) { intersect(state, 512, false); } diff --git a/cmake/common/targets/executable.cmake b/cmake/common/targets/executable.cmake index b911dc5fd..2e9c2d2b0 100644 --- a/cmake/common/targets/executable.cmake +++ b/cmake/common/targets/executable.cmake @@ -38,8 +38,8 @@ function(sourcemeta_executable) 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 all references to this - # target, including explicit allocator links on allocator test targets. + # 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() diff --git a/src/core/mcp/parameter_headers.cc b/src/core/mcp/parameter_headers.cc index c473bb587..3f433fed7 100644 --- a/src/core/mcp/parameter_headers.cc +++ b/src/core/mcp/parameter_headers.cc @@ -16,6 +16,7 @@ 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 ¶meter) -> const JSON * { @@ -145,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))); } } @@ -173,8 +175,9 @@ auto mcp_validate_parameter_headers( if (use_index) { indexed.reserve(headers.size()); for (const auto &entry : headers) { - if (starts_with_ignore_case(entry.first, "Mcp-Param-")) { - indexed.emplace_back(entry.first.substr(10), entry.second); + 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(), @@ -201,8 +204,10 @@ auto mcp_validate_parameter_headers( } } else { for (const auto &candidate : headers) { - if (starts_with_ignore_case(candidate.first, "Mcp-Param-") && - equals_ignore_case(candidate.first.substr(10), parameter.name)) { + 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"); @@ -215,7 +220,8 @@ auto mcp_validate_parameter_headers( if (value == nullptr) { if (header) { return mcp_make_error_header_mismatch( - version, &identifier, "Mcp-Param-" + std::string{parameter.name}, + version, &identifier, + std::string{PARAMETER_HEADER_PREFIX} + std::string{parameter.name}, *header); } continue; @@ -243,7 +249,8 @@ auto mcp_validate_parameter_headers( } if (!matches) { return mcp_make_error_header_mismatch( - version, &identifier, "Mcp-Param-" + std::string{parameter.name}, + version, &identifier, + std::string{PARAMETER_HEADER_PREFIX} + std::string{parameter.name}, header.value_or(""), parameter_text(*value)); } } diff --git a/test/mcp/mcp_headers_test.cc b/test/mcp/mcp_headers_test.cc index 0ed4dc77c..4c63e9422 100644 --- a/test/mcp/mcp_headers_test.cc +++ b/test/mcp/mcp_headers_test.cc @@ -6,11 +6,12 @@ #include "mcp_helpers.h" -#include // std::array -#include // std::optional, std::nullopt -#include // std::string -#include // std::pair -#include // std::vector +#include // std::reverse +#include // std::array +#include // std::optional, std::nullopt +#include // std::string +#include // std::pair +#include // std::vector TEST(request_meta_validation_valid_minimum) { const auto envelope{sourcemeta::core::parse_json(R"JSON({ @@ -4041,66 +4042,115 @@ TEST(parameter_integer_rejects_a_header_that_is_not_a_number) { sourcemeta::core::JSON{0}, descriptors, arguments, views) .has_value()); } - -TEST(parameter_headers_index_preserves_case_duplicates_and_encoding) { - using namespace sourcemeta::core; - std::vector names; - auto arguments{JSON::make_object()}; - for (std::size_t index{0}; index < 9; ++index) { - names.push_back("field" + std::to_string(index)); - arguments.assign(names.back(), JSON{" café "}); - } - std::vector parameters; - parameters.reserve(names.size()); - for (const auto &name : names) { - parameters.push_back( - {.name = name, .path = {name}, .type = JSON::Type::String}); - } - auto owned{mcp_make_parameter_headers(parameters, arguments)}; - std::vector> headers; - for (std::size_t index{0}; index < 9; ++index) { - owned[index].first.replace(0, 10, "mCp-pArAm-"); - } - for (auto entry{owned.rbegin()}; entry != owned.rend(); ++entry) { - headers.emplace_back(entry->first, entry->second); - } - // Force the indexed path; duplicate unused names must stay opaque. - for (std::size_t index{0}; index < 9; ++index) { - headers.emplace_back("Mcp-Param-unused", "ignored"); +namespace { +struct IndexedParameterHeaders { + std::vector parameters; + sourcemeta::core::JSON arguments{sourcemeta::core::JSON::make_object()}; + std::vector> owned; + std::vector> + headers; + + IndexedParameterHeaders() { + constexpr std::array NAMES{ + {"field0", "field1", "field2", "field3", "field4", "field5", "field6", + "field7", "field8"}}; + this->parameters.reserve(NAMES.size()); + for (const auto name : NAMES) { + this->parameters.push_back( + {.name = name, + .path = {name}, + .type = sourcemeta::core::JSON::Type::String}); + this->arguments.assign(name, sourcemeta::core::JSON{"plain"}); + } + this->mirror(); } - const auto version{MCPProtocolVersion::V_2026_07_28}; - const JSON identifier{1}; - EXPECT_FALSE(mcp_validate_parameter_headers(version, identifier, parameters, - arguments, headers) - .has_value()); - headers.push_back(headers.front()); - EXPECT_TRUE(mcp_validate_parameter_headers(version, identifier, parameters, - arguments, headers) - .has_value()); - headers.pop_back(); - arguments.assign("field8", JSON{true}); - EXPECT_TRUE(mcp_validate_parameter_headers(version, identifier, parameters, - arguments, headers) - .has_value()); - parameters.back().type = JSON::Type::Integer; - arguments.assign("field8", parse_json("9007199254740991.0")); - headers.clear(); - owned = mcp_make_parameter_headers(parameters, arguments); - for (const auto &entry : owned) { - headers.emplace_back(entry.first, entry.second); + + auto mirror() -> void { + this->headers.clear(); + this->owned = sourcemeta::core::mcp_make_parameter_headers(this->parameters, + this->arguments); + for (const auto &entry : this->owned) { + this->headers.emplace_back(entry.first, entry.second); + } + // Nine descriptors and eighteen headers select the indexed lookup path. + for (std::size_t index{0}; index < 9; ++index) { + this->headers.emplace_back("Other", "ignored"); + } } - for (std::size_t index{0}; index < 9; ++index) { - headers.emplace_back("Other", "ignored"); + + [[nodiscard]] auto validate() const -> std::optional { + return sourcemeta::core::mcp_validate_parameter_headers( + sourcemeta::core::MCPProtocolVersion::V_2026_07_28, + sourcemeta::core::JSON{0}, this->parameters, this->arguments, + this->headers); } - EXPECT_FALSE(mcp_validate_parameter_headers(version, identifier, parameters, - arguments, headers) - .has_value()); - arguments.assign("field8", JSON{INT64_C(9007199254740992)}); - EXPECT_TRUE(mcp_validate_parameter_headers(version, identifier, parameters, - arguments, headers) - .has_value()); - parameters.back().name = "FIELD0"; +}; +} // namespace + +TEST(parameter_headers_index_accepts_mixed_case_names) { + IndexedParameterHeaders fixture; + fixture.headers.front().first = "mCp-pArAm-FiElD0"; + EXPECT_FALSE(fixture.validate().has_value()); +} + +TEST(parameter_headers_index_accepts_reversed_header_order) { + IndexedParameterHeaders fixture; + std::reverse(fixture.headers.begin(), fixture.headers.end()); + EXPECT_FALSE(fixture.validate().has_value()); +} + +TEST(parameter_headers_index_accepts_encoded_unicode) { + IndexedParameterHeaders fixture; + fixture.arguments.assign("field8", sourcemeta::core::JSON{" café "}); + fixture.mirror(); + EXPECT_FALSE(fixture.validate().has_value()); +} + +TEST(parameter_headers_index_ignores_duplicate_unused_names) { + IndexedParameterHeaders fixture; + fixture.headers.emplace_back("Mcp-Param-unused", "ignored"); + fixture.headers.emplace_back("mcp-param-UNUSED", "different"); + EXPECT_FALSE(fixture.validate().has_value()); +} + +TEST(parameter_headers_index_rejects_duplicate_matching_names) { + IndexedParameterHeaders fixture; + fixture.headers.emplace_back("mcp-param-FIELD0", "plain"); + EXPECT_EQ(fixture.validate().value().at("error").at("message").to_string(), + "Duplicate parameter header"); +} + +TEST(parameter_headers_index_rejects_wrong_argument_type) { + IndexedParameterHeaders fixture; + fixture.arguments.assign("field8", sourcemeta::core::JSON{true}); + EXPECT_EQ(fixture.validate().value().at("error").at("message").to_string(), + "Invalid mirrored argument"); +} + +TEST(parameter_headers_index_accepts_safe_integral_decimal) { + IndexedParameterHeaders fixture; + fixture.parameters.back().type = sourcemeta::core::JSON::Type::Integer; + fixture.arguments.assign("field8", + sourcemeta::core::parse_json("9007199254740991.0")); + fixture.mirror(); + EXPECT_FALSE(fixture.validate().has_value()); +} + +TEST(parameter_headers_index_rejects_unsafe_integer) { + IndexedParameterHeaders fixture; + fixture.parameters.back().type = sourcemeta::core::JSON::Type::Integer; + fixture.arguments.assign("field8", + sourcemeta::core::JSON{INT64_C(9007199254740992)}); + fixture.headers.at(8).second = "9007199254740992"; + EXPECT_EQ(fixture.validate().value().at("error").at("message").to_string(), + "Invalid mirrored argument"); +} + +TEST(parameter_headers_index_rejects_duplicate_descriptor_names) { + IndexedParameterHeaders fixture; + fixture.parameters.back().name = "FIELD0"; EXPECT_MCP_REJECTS( - mcp_make_parameter_headers(parameters, arguments), + fixture.validate(), "Parameter header names must be case-insensitively unique"); }