From b7c142567b1057bd4f283d3c199c01d0503b27ee Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 19:48:53 -0400 Subject: [PATCH 1/6] Add a new MCP module Signed-off-by: Juan Cruz Viotti --- .github/workflows/website-build.yml | 1 + .github/workflows/website-deploy.yml | 1 + CMakeLists.txt | 9 + config.cmake.in | 9 + src/core/mcp/CMakeLists.txt | 9 + src/core/mcp/include/sourcemeta/core/mcp.h | 547 ++++++++++++ src/core/mcp/mcp.cc | 410 +++++++++ test/mcp/CMakeLists.txt | 5 + test/mcp/mcp_test.cc | 924 +++++++++++++++++++++ test/packaging/find_package/CMakeLists.txt | 1 + test/packaging/find_package/hello.cc | 1 + 11 files changed, 1917 insertions(+) create mode 100644 src/core/mcp/CMakeLists.txt create mode 100644 src/core/mcp/include/sourcemeta/core/mcp.h create mode 100644 src/core/mcp/mcp.cc create mode 100644 test/mcp/CMakeLists.txt create mode 100644 test/mcp/mcp_test.cc diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index 978a6a01a..f25435563 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -39,6 +39,7 @@ jobs: -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF -DSOURCEMETA_CORE_JSONRPC:BOOL=OFF + -DSOURCEMETA_CORE_MCP:BOOL=OFF -DSOURCEMETA_CORE_SEMVER:BOOL=OFF -DSOURCEMETA_CORE_GZIP:BOOL=OFF -DSOURCEMETA_CORE_HTML:BOOL=OFF diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index b814ad807..eee6b3702 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -49,6 +49,7 @@ jobs: -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF -DSOURCEMETA_CORE_JSONRPC:BOOL=OFF + -DSOURCEMETA_CORE_MCP:BOOL=OFF -DSOURCEMETA_CORE_SEMVER:BOOL=OFF -DSOURCEMETA_CORE_GZIP:BOOL=OFF -DSOURCEMETA_CORE_HTML:BOOL=OFF diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c8c05bce..79a2302ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ option(SOURCEMETA_CORE_JSONPOINTER "Build the Sourcemeta Core JSON Pointer libra option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON) +option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_SEMVER "Build the Sourcemeta Core SemVer library" ON) option(SOURCEMETA_CORE_GZIP "Build the Sourcemeta Core GZIP library" ON) option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) @@ -168,6 +169,10 @@ if(SOURCEMETA_CORE_JSONRPC) add_subdirectory(src/core/jsonrpc) endif() +if(SOURCEMETA_CORE_MCP) + add_subdirectory(src/core/mcp) +endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(src/core/semver) endif() @@ -304,6 +309,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/jsonrpc) endif() + if(SOURCEMETA_CORE_MCP) + add_subdirectory(test/mcp) + endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(test/semver) endif() diff --git a/config.cmake.in b/config.cmake.in index 18a7effdb..912fd639e 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -24,6 +24,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonpointer) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc) + list(APPEND SOURCEMETA_CORE_COMPONENTS mcp) list(APPEND SOURCEMETA_CORE_COMPONENTS semver) list(APPEND SOURCEMETA_CORE_COMPONENTS gzip) list(APPEND SOURCEMETA_CORE_COMPONENTS html) @@ -120,6 +121,14 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") + elseif(component STREQUAL "mcp") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_mcp.cmake") elseif(component STREQUAL "semver") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_semver.cmake") diff --git a/src/core/mcp/CMakeLists.txt b/src/core/mcp/CMakeLists.txt new file mode 100644 index 000000000..a76973d0e --- /dev/null +++ b/src/core/mcp/CMakeLists.txt @@ -0,0 +1,9 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME mcp + SOURCES mcp.cc) + +target_link_libraries(sourcemeta_core_mcp PUBLIC sourcemeta::core::json) +target_link_libraries(sourcemeta_core_mcp PUBLIC sourcemeta::core::jsonrpc) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME mcp) +endif() diff --git a/src/core/mcp/include/sourcemeta/core/mcp.h b/src/core/mcp/include/sourcemeta/core/mcp.h new file mode 100644 index 000000000..53bb2ca18 --- /dev/null +++ b/src/core/mcp/include/sourcemeta/core/mcp.h @@ -0,0 +1,547 @@ +#ifndef SOURCEMETA_CORE_MCP_H_ +#define SOURCEMETA_CORE_MCP_H_ + +#ifndef SOURCEMETA_CORE_MCP_EXPORT +#include +#endif + +#include +#include + +#include // std::size_t +#include // std::int64_t, std::uint8_t +#include // std::optional, std::nullopt +#include // std::string_view +#include // std::unreachable + +/// @defgroup mcp MCP +/// @brief Helpers for building Model Context Protocol (MCP) envelopes on top of +/// JSON-RPC 2.0. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +namespace sourcemeta::core { + +/// @ingroup mcp +/// The supported MCP protocol revisions. +enum class MCPProtocolVersion : std::uint8_t { + V_2025_03_26, + V_2025_06_18, + V_2025_11_25, +}; + +/// @ingroup mcp +/// Get the canonical wire-format string for an MCP protocol version. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::mcp_protocol_version_string( +/// sourcemeta::core::MCPProtocolVersion::V_2025_11_25) == +/// "2025-11-25"); +/// ``` +constexpr auto +mcp_protocol_version_string(const MCPProtocolVersion version) noexcept + -> std::string_view { + switch (version) { + case MCPProtocolVersion::V_2025_03_26: + return "2025-03-26"; + case MCPProtocolVersion::V_2025_06_18: + return "2025-06-18"; + case MCPProtocolVersion::V_2025_11_25: + return "2025-11-25"; + } + std::unreachable(); +} + +/// @ingroup mcp +/// The MCP method name for the `initialize` request. +constexpr std::string_view MCP_METHOD_INITIALIZE{"initialize"}; + +/// @ingroup mcp +/// The MCP method name for the `ping` request. +constexpr std::string_view MCP_METHOD_PING{"ping"}; + +/// @ingroup mcp +/// The MCP method name for the `tools/list` request. +constexpr std::string_view MCP_METHOD_TOOLS_LIST{"tools/list"}; + +/// @ingroup mcp +/// The MCP method name for the `tools/call` request. +constexpr std::string_view MCP_METHOD_TOOLS_CALL{"tools/call"}; + +/// @ingroup mcp +/// The MCP method name for the `resources/list` request. +constexpr std::string_view MCP_METHOD_RESOURCES_LIST{"resources/list"}; + +/// @ingroup mcp +/// The MCP method name for the `resources/read` request. +constexpr std::string_view MCP_METHOD_RESOURCES_READ{"resources/read"}; + +/// @ingroup mcp +/// The MCP method name for the `resources/templates/list` request. +constexpr std::string_view MCP_METHOD_RESOURCES_TEMPLATES_LIST{ + "resources/templates/list"}; + +/// @ingroup mcp +/// The MCP method name for the `notifications/initialized` notification. +constexpr std::string_view MCP_METHOD_NOTIFICATIONS_INITIALIZED{ + "notifications/initialized"}; + +/// @ingroup mcp +/// The MCP error code returned when a requested resource cannot be found. +constexpr std::int64_t MCP_CODE_RESOURCE_NOT_FOUND{-32002}; + +/// @ingroup mcp +/// The MCP error code indicating that the client must complete a URL +/// elicitation flow before retrying. +constexpr std::int64_t MCP_CODE_URL_ELICITATION_REQUIRED{-32042}; + +/// @ingroup mcp +/// Check whether the given method name corresponds to an MCP request method +/// (notifications excluded). For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::mcp_is_request_method("initialize")); +/// assert(!sourcemeta::core::mcp_is_request_method("notifications/initialized")); +/// ``` +constexpr auto mcp_is_request_method(const std::string_view method) noexcept + -> bool { + return method == MCP_METHOD_INITIALIZE || method == MCP_METHOD_PING || + method == MCP_METHOD_TOOLS_LIST || method == MCP_METHOD_TOOLS_CALL || + method == MCP_METHOD_RESOURCES_LIST || + method == MCP_METHOD_RESOURCES_READ || + method == MCP_METHOD_RESOURCES_TEMPLATES_LIST; +} + +/// @ingroup mcp +/// Resolve an `MCP-Protocol-Version` header value into a known protocol +/// version. An empty header defaults to the earliest revision, per the MCP +/// specification. Returns `std::nullopt` when the header is unrecognised. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto resolved{ +/// sourcemeta::core::mcp_resolve_protocol_version("2025-11-25")}; +/// assert(resolved.has_value()); +/// assert(resolved.value() == +/// sourcemeta::core::MCPProtocolVersion::V_2025_11_25); +/// ``` +constexpr auto +mcp_resolve_protocol_version(const std::string_view header) noexcept + -> std::optional { + if (header.empty()) { + return MCPProtocolVersion::V_2025_03_26; + } + if (header == "2025-11-25") { + return MCPProtocolVersion::V_2025_11_25; + } + if (header == "2025-06-18") { + return MCPProtocolVersion::V_2025_06_18; + } + if (header == "2025-03-26") { + return MCPProtocolVersion::V_2025_03_26; + } + return std::nullopt; +} + +/// @ingroup mcp +/// Whether the given protocol version supports per-tool `outputSchema`. +constexpr auto +mcp_supports_output_schema(const MCPProtocolVersion version) noexcept -> bool { + return version != MCPProtocolVersion::V_2025_03_26; +} + +/// @ingroup mcp +/// Whether the given protocol version supports `structuredContent` in tool +/// results. +constexpr auto +mcp_supports_structured_content(const MCPProtocolVersion version) noexcept + -> bool { + return version != MCPProtocolVersion::V_2025_03_26; +} + +/// @ingroup mcp +/// Whether the given protocol version supports `resource_link` content blocks. +constexpr auto +mcp_supports_resource_link_content(const MCPProtocolVersion version) noexcept + -> bool { + return version != MCPProtocolVersion::V_2025_03_26; +} + +/// @ingroup mcp +/// Whether the given protocol version supports the `title` field on the +/// implementation info object. +constexpr auto +mcp_supports_implementation_title(const MCPProtocolVersion version) noexcept + -> bool { + return version != MCPProtocolVersion::V_2025_03_26; +} + +/// @ingroup mcp +/// Whether the given protocol version supports the `description` field on the +/// implementation info object. +constexpr auto mcp_supports_implementation_description( + const MCPProtocolVersion version) noexcept -> bool { + return version == MCPProtocolVersion::V_2025_11_25; +} + +/// @ingroup mcp +/// Whether the given protocol version supports the `websiteUrl` field on the +/// implementation info object. +constexpr auto mcp_supports_implementation_website_url( + const MCPProtocolVersion version) noexcept -> bool { + return version == MCPProtocolVersion::V_2025_11_25; +} + +/// @ingroup mcp +/// Build an MCP `text` content block carrying the given text payload. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto block{sourcemeta::core::mcp_make_text_block("hello")}; +/// assert(block.at("type").to_string() == "text"); +/// assert(block.at("text").to_string() == "hello"); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build an MCP `resource_link` content block, or a `text` content block +/// fallback when the protocol version does not support the link form. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto block{sourcemeta::core::mcp_make_resource_link( +/// sourcemeta::core::MCPProtocolVersion::V_2025_11_25, "file:///foo", +/// "text/plain")}; +/// assert(block.at("type").to_string() == "resource_link"); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_resource_link(const MCPProtocolVersion version, + const std::string_view uri, + const std::string_view mime_type, + const std::string_view name = {}, + const std::string_view description = {}) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build a JSON-RPC envelope wrapping a successful MCP tool call response. The +/// `result` is serialised as a `text` content block and additionally copied +/// into `structuredContent` when the protocol version supports it. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{1}}; +/// auto result{sourcemeta::core::JSON::make_object()}; +/// result.assign("foo", sourcemeta::core::JSON{42}); +/// const auto envelope{sourcemeta::core::mcp_make_tool_success( +/// sourcemeta::core::MCPProtocolVersion::V_2025_11_25, identifier, +/// std::move(result))}; +/// assert(envelope.at("result").at("isError").to_boolean() == false); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_tool_success(const MCPProtocolVersion version, + const sourcemeta::core::JSON &identifier, + sourcemeta::core::JSON result) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build a JSON-RPC envelope wrapping a successful MCP tool call response with +/// caller-provided content blocks and structured payload. The structured +/// payload is included only when the protocol version supports it. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{1}}; +/// auto structured{sourcemeta::core::JSON::make_object()}; +/// structured.assign("ok", sourcemeta::core::JSON{true}); +/// auto blocks{sourcemeta::core::JSON::make_array()}; +/// blocks.push_back(sourcemeta::core::mcp_make_text_block("done")); +/// const auto envelope{sourcemeta::core::mcp_make_tool_success( +/// sourcemeta::core::MCPProtocolVersion::V_2025_11_25, identifier, +/// std::move(structured), std::move(blocks))}; +/// assert(envelope.at("result").at("isError").to_boolean() == false); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_tool_success(const MCPProtocolVersion version, + const sourcemeta::core::JSON &identifier, + sourcemeta::core::JSON structured, + sourcemeta::core::JSON content_blocks) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build a JSON-RPC envelope wrapping a failed MCP tool call response. The +/// `isError` field is set to `true` and `content` carries a single `text` +/// block. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{1}}; +/// const auto envelope{ +/// sourcemeta::core::mcp_make_tool_error(identifier, "Boom")}; +/// assert(envelope.at("result").at("isError").to_boolean() == true); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, + const std::string_view message) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build a JSON-RPC error envelope using the MCP "resource not found" code +/// and the offending URI as the `data` field. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{3}}; +/// const auto envelope{sourcemeta::core::mcp_make_error_resource_not_found( +/// identifier, "file:///missing")}; +/// assert(envelope.at("error").at("code").to_integer() == -32002); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &identifier, + const std::string_view uri) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build an MCP resource descriptor as used in `resources/list` responses. +/// The `description` is omitted when empty and the `size` is omitted when +/// unset. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto resource{sourcemeta::core::mcp_make_resource( +/// "file:///a", "Alpha", "text/plain")}; +/// assert(resource.at("uri").to_string() == "file:///a"); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_resource(const std::string_view uri, const std::string_view name, + const std::string_view mime_type, + const std::string_view description = {}, + const std::optional size = std::nullopt) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build an MCP `resources/read` content entry of `text` flavour. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto content{sourcemeta::core::mcp_make_resource_text_content( +/// "file:///a", "text/plain", "Hello")}; +/// assert(content.at("text").to_string() == "Hello"); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_resource_text_content(const std::string_view uri, + const std::string_view mime_type, + const std::string_view text) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Wrap a pre-built array of content entries into the MCP `resources/read` +/// result envelope. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// auto contents{sourcemeta::core::JSON::make_array()}; +/// contents.push_back(sourcemeta::core::mcp_make_resource_text_content( +/// "file:///a", "text/plain", "Hello")); +/// const auto result{ +/// sourcemeta::core::mcp_make_resources_read_result(std::move(contents))}; +/// assert(result.at("contents").size() == 1); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_resources_read_result(sourcemeta::core::JSON contents) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build a single entry for an MCP `resources/templates/list` response. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto entry{sourcemeta::core::mcp_make_resource_template( +/// "file:///{path}", "Files", "Resolves a file path", "text/plain")}; +/// assert(entry.at("uriTemplate").to_string() == "file:///{path}"); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_resource_template(const std::string_view uri_template, + const std::string_view name, + const std::string_view description, + const std::string_view mime_type) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Optional hints attached to an MCP tool descriptor. Field semantics follow +/// the MCP specification: defaults reflect the worst-case assumptions (a +/// destructive, open-world, non-idempotent tool). +struct MCPToolAnnotations { + /// Optional human-readable title for the tool. + std::string_view title = {}; + /// `true` when the tool guarantees no side effects. + bool read_only = false; + /// `true` when the tool may mutate or delete state. + bool destructive = true; + /// `true` when repeated invocations with the same input yield the same + /// result. + bool idempotent = false; + /// `true` when the tool interacts with state outside the server's control. + bool open_world = true; +}; + +/// @ingroup mcp +/// Build a single entry for an MCP `tools/list` response. The `outputSchema` +/// is dropped automatically when the protocol version predates its support. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto entry{sourcemeta::core::mcp_make_tool_descriptor( +/// sourcemeta::core::MCPProtocolVersion::V_2025_11_25, "say", "Says hello", +/// sourcemeta::core::parse_json(R"({ "type": "object" })"))}; +/// assert(entry.at("name").to_string() == "say"); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_tool_descriptor( + const MCPProtocolVersion version, const std::string_view name, + const std::string_view description, sourcemeta::core::JSON input_schema, + std::optional output_schema = std::nullopt, + const MCPToolAnnotations &annotations = {}) -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Implementation info advertised by an MCP server during the initialize +/// handshake. Fields that the negotiated protocol version does not support +/// are dropped automatically. +struct MCPImplementation { + /// Short machine-readable server name. + std::string_view name; + /// Semver-compatible server version. + std::string_view version; + /// Optional human-readable title. + std::string_view title = {}; + /// Optional human-readable description. + std::string_view description = {}; + /// Optional public website URL. + std::string_view website_url = {}; +}; + +/// @ingroup mcp +/// Boolean toggles for the MCP `capabilities` object returned during the +/// initialize handshake. Each toggle gates the presence of an empty object +/// under the corresponding key. +struct MCPServerCapabilities { + /// Whether the server advertises prompts. + bool prompts = false; + /// Whether the server advertises resources. + bool resources = false; + /// Whether the server advertises tools. + bool tools = false; + /// Whether the server advertises logging. + bool logging = false; + /// Whether the server advertises completions. + bool completions = false; +}; + +/// @ingroup mcp +/// Build the JSON-RPC envelope returned in response to an MCP `initialize` +/// request. Returns a JSON-RPC invalid-request envelope when the incoming +/// request lacks an identifier or a `params` object. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto request{sourcemeta::core::parse_json(R"JSON({ +/// "jsonrpc": "2.0", +/// "id": 1, +/// "method": "initialize", +/// "params": { "protocolVersion": "2025-11-25" } +/// })JSON")}; +/// const sourcemeta::core::MCPServerCapabilities capabilities; +/// const sourcemeta::core::MCPImplementation server{"srv", "1.0.0"}; +/// const auto envelope{sourcemeta::core::mcp_make_initialize_result( +/// request, capabilities, server)}; +/// assert(envelope.at("result").at("protocolVersion").to_string() == +/// "2025-11-25"); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, + const MCPServerCapabilities &capabilities, + const MCPImplementation &server, + const std::string_view instructions = {}) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Borrow the `arguments` object from a JSON-RPC `tools/call` envelope, or +/// return `nullptr` when it is missing or `params` is not an object. The +/// returned pointer is valid for the lifetime of the input envelope. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto envelope{sourcemeta::core::parse_json(R"JSON({ +/// "jsonrpc": "2.0", +/// "id": 1, +/// "method": "tools/call", +/// "params": { "name": "foo", "arguments": { "x": 1 } } +/// })JSON")}; +/// const auto *arguments{sourcemeta::core::mcp_tool_call_arguments(envelope)}; +/// assert(arguments != nullptr); +/// assert(arguments->at("x").to_integer() == 1); +/// ``` +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) + -> const sourcemeta::core::JSON *; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/mcp/mcp.cc b/src/core/mcp/mcp.cc new file mode 100644 index 000000000..d05f67874 --- /dev/null +++ b/src/core/mcp/mcp.cc @@ -0,0 +1,410 @@ +#include + +#include +#include + +#include // assert +#include // std::size_t +#include // std::optional +#include // std::ostringstream +#include // std::string +#include // std::string_view +#include // std::move + +namespace { + +const auto MCP_HASH_ANNOTATIONS{ + sourcemeta::core::JSON::Object::hash("annotations")}; +const auto MCP_HASH_ARGUMENTS{ + sourcemeta::core::JSON::Object::hash("arguments")}; +const auto MCP_HASH_CAPABILITIES{ + sourcemeta::core::JSON::Object::hash("capabilities")}; +const auto MCP_HASH_COMPLETIONS{ + sourcemeta::core::JSON::Object::hash("completions")}; +const auto MCP_HASH_CONTENT{sourcemeta::core::JSON::Object::hash("content")}; +const auto MCP_HASH_CONTENTS{sourcemeta::core::JSON::Object::hash("contents")}; +const auto MCP_HASH_DESCRIPTION{ + sourcemeta::core::JSON::Object::hash("description")}; +const auto MCP_HASH_DESTRUCTIVE_HINT{ + sourcemeta::core::JSON::Object::hash("destructiveHint")}; +const auto MCP_HASH_IDEMPOTENT_HINT{ + sourcemeta::core::JSON::Object::hash("idempotentHint")}; +const auto MCP_HASH_INPUT_SCHEMA{ + sourcemeta::core::JSON::Object::hash("inputSchema")}; +const auto MCP_HASH_INSTRUCTIONS{ + sourcemeta::core::JSON::Object::hash("instructions")}; +const auto MCP_HASH_IS_ERROR{sourcemeta::core::JSON::Object::hash("isError")}; +const auto MCP_HASH_LOGGING{sourcemeta::core::JSON::Object::hash("logging")}; +const auto MCP_HASH_MIME_TYPE{sourcemeta::core::JSON::Object::hash("mimeType")}; +const auto MCP_HASH_NAME{sourcemeta::core::JSON::Object::hash("name")}; +const auto MCP_HASH_OPEN_WORLD_HINT{ + sourcemeta::core::JSON::Object::hash("openWorldHint")}; +const auto MCP_HASH_OUTPUT_SCHEMA{ + sourcemeta::core::JSON::Object::hash("outputSchema")}; +const auto MCP_HASH_PROMPTS{sourcemeta::core::JSON::Object::hash("prompts")}; +const auto MCP_HASH_PROTOCOL_VERSION{ + sourcemeta::core::JSON::Object::hash("protocolVersion")}; +const auto MCP_HASH_READ_ONLY_HINT{ + sourcemeta::core::JSON::Object::hash("readOnlyHint")}; +const auto MCP_HASH_RESOURCES{ + sourcemeta::core::JSON::Object::hash("resources")}; +const auto MCP_HASH_SERVER_INFO{ + sourcemeta::core::JSON::Object::hash("serverInfo")}; +const auto MCP_HASH_SIZE{sourcemeta::core::JSON::Object::hash("size")}; +const auto MCP_HASH_STRUCTURED_CONTENT{ + sourcemeta::core::JSON::Object::hash("structuredContent")}; +const auto MCP_HASH_TEXT{sourcemeta::core::JSON::Object::hash("text")}; +const auto MCP_HASH_TITLE{sourcemeta::core::JSON::Object::hash("title")}; +const auto MCP_HASH_TOOLS{sourcemeta::core::JSON::Object::hash("tools")}; +const auto MCP_HASH_TYPE{sourcemeta::core::JSON::Object::hash("type")}; +const auto MCP_HASH_URI{sourcemeta::core::JSON::Object::hash("uri")}; +const auto MCP_HASH_URI_TEMPLATE{ + sourcemeta::core::JSON::Object::hash("uriTemplate")}; +const auto MCP_HASH_VERSION{sourcemeta::core::JSON::Object::hash("version")}; +const auto MCP_HASH_WEBSITE_URL{ + sourcemeta::core::JSON::Object::hash("websiteUrl")}; + +} // namespace + +namespace sourcemeta::core { + +auto mcp_make_text_block(const std::string_view text) + -> sourcemeta::core::JSON { + auto block{sourcemeta::core::JSON::make_object()}; + block.assign_assume_new(std::string{"type"}, sourcemeta::core::JSON{"text"}, + MCP_HASH_TYPE); + block.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}, + MCP_HASH_TEXT); + return block; +} + +auto mcp_make_resource_link(const MCPProtocolVersion version, + const std::string_view uri, + const std::string_view mime_type, + const std::string_view name, + const std::string_view description) + -> sourcemeta::core::JSON { + if (!mcp_supports_resource_link_content(version)) { + std::string text; + if (!name.empty()) { + text.append(name); + if (!description.empty()) { + text.append(" ("); + text.append(description); + text.append(")"); + } + text.append(": "); + } else if (!description.empty()) { + text.append(description); + text.append(": "); + } + text.append(uri); + return mcp_make_text_block(text); + } + + auto block{sourcemeta::core::JSON::make_object()}; + block.assign_assume_new(std::string{"type"}, + sourcemeta::core::JSON{"resource_link"}, + MCP_HASH_TYPE); + block.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, + MCP_HASH_URI); + if (!name.empty()) { + block.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); + } + if (!description.empty()) { + block.assign_assume_new(std::string{"description"}, + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); + } + block.assign_assume_new(std::string{"mimeType"}, + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + return block; +} + +auto mcp_make_tool_success(const MCPProtocolVersion version, + const sourcemeta::core::JSON &identifier, + sourcemeta::core::JSON result) + -> sourcemeta::core::JSON { + std::ostringstream payload; + sourcemeta::core::prettify(result, payload); + + auto content{sourcemeta::core::JSON::make_array()}; + content.push_back(mcp_make_text_block(payload.str())); + + auto envelope_result{sourcemeta::core::JSON::make_object()}; + envelope_result.assign_assume_new(std::string{"content"}, std::move(content), + MCP_HASH_CONTENT); + if (mcp_supports_structured_content(version)) { + envelope_result.assign_assume_new(std::string{"structuredContent"}, + std::move(result), + MCP_HASH_STRUCTURED_CONTENT); + } + envelope_result.assign_assume_new( + std::string{"isError"}, sourcemeta::core::JSON{false}, MCP_HASH_IS_ERROR); + return sourcemeta::core::jsonrpc_make_success(identifier, + std::move(envelope_result)); +} + +auto mcp_make_tool_success(const MCPProtocolVersion version, + const sourcemeta::core::JSON &identifier, + sourcemeta::core::JSON structured, + sourcemeta::core::JSON content_blocks) + -> sourcemeta::core::JSON { + auto envelope_result{sourcemeta::core::JSON::make_object()}; + envelope_result.assign_assume_new( + std::string{"content"}, std::move(content_blocks), MCP_HASH_CONTENT); + if (mcp_supports_structured_content(version)) { + envelope_result.assign_assume_new(std::string{"structuredContent"}, + std::move(structured), + MCP_HASH_STRUCTURED_CONTENT); + } + envelope_result.assign_assume_new( + std::string{"isError"}, sourcemeta::core::JSON{false}, MCP_HASH_IS_ERROR); + return sourcemeta::core::jsonrpc_make_success(identifier, + std::move(envelope_result)); +} + +auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, + const std::string_view message) + -> sourcemeta::core::JSON { + auto content{sourcemeta::core::JSON::make_array()}; + content.push_back(mcp_make_text_block(message)); + + auto envelope_result{sourcemeta::core::JSON::make_object()}; + envelope_result.assign_assume_new(std::string{"content"}, std::move(content), + MCP_HASH_CONTENT); + envelope_result.assign_assume_new( + std::string{"isError"}, sourcemeta::core::JSON{true}, MCP_HASH_IS_ERROR); + return sourcemeta::core::jsonrpc_make_success(identifier, + std::move(envelope_result)); +} + +auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &identifier, + const std::string_view uri) + -> sourcemeta::core::JSON { + return sourcemeta::core::jsonrpc_make_error( + &identifier, MCP_CODE_RESOURCE_NOT_FOUND, "Resource not found", + sourcemeta::core::JSON{uri}); +} + +auto mcp_make_resource(const std::string_view uri, const std::string_view name, + const std::string_view mime_type, + const std::string_view description, + const std::optional size) + -> sourcemeta::core::JSON { + auto resource{sourcemeta::core::JSON::make_object()}; + resource.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, + MCP_HASH_URI); + resource.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); + if (!description.empty()) { + resource.assign_assume_new(std::string{"description"}, + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); + } + resource.assign_assume_new(std::string{"mimeType"}, + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + if (size.has_value()) { + resource.assign_assume_new(std::string{"size"}, + sourcemeta::core::JSON{size.value()}, + MCP_HASH_SIZE); + } + return resource; +} + +auto mcp_make_resource_text_content(const std::string_view uri, + const std::string_view mime_type, + const std::string_view text) + -> sourcemeta::core::JSON { + auto entry{sourcemeta::core::JSON::make_object()}; + entry.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, + MCP_HASH_URI); + entry.assign_assume_new(std::string{"mimeType"}, + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + entry.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}, + MCP_HASH_TEXT); + return entry; +} + +auto mcp_make_resources_read_result(sourcemeta::core::JSON contents) + -> sourcemeta::core::JSON { + auto result{sourcemeta::core::JSON::make_object()}; + result.assign_assume_new(std::string{"contents"}, std::move(contents), + MCP_HASH_CONTENTS); + return result; +} + +auto mcp_make_resource_template(const std::string_view uri_template, + const std::string_view name, + const std::string_view description, + const std::string_view mime_type) + -> sourcemeta::core::JSON { + auto entry{sourcemeta::core::JSON::make_object()}; + entry.assign_assume_new(std::string{"uriTemplate"}, + sourcemeta::core::JSON{uri_template}, + MCP_HASH_URI_TEMPLATE); + entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); + entry.assign_assume_new(std::string{"description"}, + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); + entry.assign_assume_new(std::string{"mimeType"}, + sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + return entry; +} + +auto mcp_make_tool_descriptor( + const MCPProtocolVersion version, const std::string_view name, + const std::string_view description, sourcemeta::core::JSON input_schema, + std::optional output_schema, + const MCPToolAnnotations &annotations) -> sourcemeta::core::JSON { + assert(!annotations.read_only || !annotations.destructive); + assert(!annotations.read_only || annotations.idempotent); + + auto entry{sourcemeta::core::JSON::make_object()}; + entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + MCP_HASH_NAME); + entry.assign_assume_new(std::string{"description"}, + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); + entry.assign_assume_new(std::string{"inputSchema"}, std::move(input_schema), + MCP_HASH_INPUT_SCHEMA); + if (output_schema.has_value() && mcp_supports_output_schema(version)) { + entry.assign_assume_new(std::string{"outputSchema"}, + std::move(output_schema).value(), + MCP_HASH_OUTPUT_SCHEMA); + } + + auto annotations_object{sourcemeta::core::JSON::make_object()}; + if (!annotations.title.empty()) { + annotations_object.assign_assume_new( + std::string{"title"}, sourcemeta::core::JSON{annotations.title}, + MCP_HASH_TITLE); + } + annotations_object.assign_assume_new( + std::string{"readOnlyHint"}, + sourcemeta::core::JSON{annotations.read_only}, MCP_HASH_READ_ONLY_HINT); + annotations_object.assign_assume_new( + std::string{"destructiveHint"}, + sourcemeta::core::JSON{annotations.destructive}, + MCP_HASH_DESTRUCTIVE_HINT); + annotations_object.assign_assume_new( + std::string{"idempotentHint"}, + sourcemeta::core::JSON{annotations.idempotent}, MCP_HASH_IDEMPOTENT_HINT); + annotations_object.assign_assume_new( + std::string{"openWorldHint"}, + sourcemeta::core::JSON{annotations.open_world}, MCP_HASH_OPEN_WORLD_HINT); + entry.assign_assume_new(std::string{"annotations"}, + std::move(annotations_object), MCP_HASH_ANNOTATIONS); + + return entry; +} + +auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, + const MCPServerCapabilities &capabilities, + const MCPImplementation &server, + const std::string_view instructions) + -> sourcemeta::core::JSON { + const auto *identifier{sourcemeta::core::jsonrpc_request_id(request)}; + const auto *parameters{sourcemeta::core::jsonrpc_params(request)}; + if (identifier == nullptr || parameters == nullptr || + !parameters->is_object()) { + return sourcemeta::core::jsonrpc_make_error_invalid_request(identifier); + } + + std::string_view requested_version{}; + if (parameters->defines("protocolVersion", MCP_HASH_PROTOCOL_VERSION) && + parameters->at("protocolVersion", MCP_HASH_PROTOCOL_VERSION) + .is_string()) { + requested_version = + parameters->at("protocolVersion", MCP_HASH_PROTOCOL_VERSION) + .to_string(); + } + const auto resolved{mcp_resolve_protocol_version(requested_version)}; + const auto version{resolved.value_or(MCPProtocolVersion::V_2025_11_25)}; + + auto capabilities_object{sourcemeta::core::JSON::make_object()}; + if (capabilities.prompts) { + capabilities_object.assign_assume_new(std::string{"prompts"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_PROMPTS); + } + if (capabilities.resources) { + capabilities_object.assign_assume_new(std::string{"resources"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_RESOURCES); + } + if (capabilities.tools) { + capabilities_object.assign_assume_new(std::string{"tools"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_TOOLS); + } + if (capabilities.logging) { + capabilities_object.assign_assume_new(std::string{"logging"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_LOGGING); + } + if (capabilities.completions) { + capabilities_object.assign_assume_new(std::string{"completions"}, + sourcemeta::core::JSON::make_object(), + MCP_HASH_COMPLETIONS); + } + + auto server_info{sourcemeta::core::JSON::make_object()}; + server_info.assign_assume_new( + std::string{"name"}, sourcemeta::core::JSON{server.name}, MCP_HASH_NAME); + server_info.assign_assume_new(std::string{"version"}, + sourcemeta::core::JSON{server.version}, + MCP_HASH_VERSION); + if (!server.title.empty() && mcp_supports_implementation_title(version)) { + server_info.assign_assume_new(std::string{"title"}, + sourcemeta::core::JSON{server.title}, + MCP_HASH_TITLE); + } + if (!server.description.empty() && + mcp_supports_implementation_description(version)) { + server_info.assign_assume_new(std::string{"description"}, + sourcemeta::core::JSON{server.description}, + MCP_HASH_DESCRIPTION); + } + if (!server.website_url.empty() && + mcp_supports_implementation_website_url(version)) { + server_info.assign_assume_new(std::string{"websiteUrl"}, + sourcemeta::core::JSON{server.website_url}, + MCP_HASH_WEBSITE_URL); + } + + auto result{sourcemeta::core::JSON::make_object()}; + result.assign_assume_new( + std::string{"protocolVersion"}, + sourcemeta::core::JSON{mcp_protocol_version_string(version)}, + MCP_HASH_PROTOCOL_VERSION); + result.assign_assume_new(std::string{"capabilities"}, + std::move(capabilities_object), + MCP_HASH_CAPABILITIES); + result.assign_assume_new(std::string{"serverInfo"}, std::move(server_info), + MCP_HASH_SERVER_INFO); + if (!instructions.empty()) { + result.assign_assume_new(std::string{"instructions"}, + sourcemeta::core::JSON{instructions}, + MCP_HASH_INSTRUCTIONS); + } + return sourcemeta::core::jsonrpc_make_success(*identifier, std::move(result)); +} + +auto mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) + -> const sourcemeta::core::JSON * { + const auto *parameters{sourcemeta::core::jsonrpc_params(envelope)}; + if (parameters == nullptr || !parameters->is_object() || + !parameters->defines("arguments", MCP_HASH_ARGUMENTS)) { + return nullptr; + } + return ¶meters->at("arguments", MCP_HASH_ARGUMENTS); +} + +} // namespace sourcemeta::core diff --git a/test/mcp/CMakeLists.txt b/test/mcp/CMakeLists.txt new file mode 100644 index 000000000..4ea39c893 --- /dev/null +++ b/test/mcp/CMakeLists.txt @@ -0,0 +1,5 @@ +sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME mcp + SOURCES mcp_test.cc) + +target_link_libraries(sourcemeta_core_mcp_unit + PRIVATE sourcemeta::core::mcp) diff --git a/test/mcp/mcp_test.cc b/test/mcp/mcp_test.cc new file mode 100644 index 000000000..8be8177ae --- /dev/null +++ b/test/mcp/mcp_test.cc @@ -0,0 +1,924 @@ +#include + +#include +#include + +#include + +#include // std::size_t +#include // std::int64_t +#include // std::optional, std::nullopt +#include // std::move + +TEST(MCP, protocol_version_string_2025_03_26) { + EXPECT_EQ(sourcemeta::core::mcp_protocol_version_string( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26), + "2025-03-26"); +} + +TEST(MCP, protocol_version_string_2025_06_18) { + EXPECT_EQ(sourcemeta::core::mcp_protocol_version_string( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18), + "2025-06-18"); +} + +TEST(MCP, protocol_version_string_2025_11_25) { + EXPECT_EQ(sourcemeta::core::mcp_protocol_version_string( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25), + "2025-11-25"); +} + +TEST(MCP, method_initialize) { + EXPECT_EQ(sourcemeta::core::MCP_METHOD_INITIALIZE, "initialize"); +} + +TEST(MCP, method_ping) { EXPECT_EQ(sourcemeta::core::MCP_METHOD_PING, "ping"); } + +TEST(MCP, method_tools_list) { + EXPECT_EQ(sourcemeta::core::MCP_METHOD_TOOLS_LIST, "tools/list"); +} + +TEST(MCP, method_tools_call) { + EXPECT_EQ(sourcemeta::core::MCP_METHOD_TOOLS_CALL, "tools/call"); +} + +TEST(MCP, method_resources_list) { + EXPECT_EQ(sourcemeta::core::MCP_METHOD_RESOURCES_LIST, "resources/list"); +} + +TEST(MCP, method_resources_read) { + EXPECT_EQ(sourcemeta::core::MCP_METHOD_RESOURCES_READ, "resources/read"); +} + +TEST(MCP, method_resources_templates_list) { + EXPECT_EQ(sourcemeta::core::MCP_METHOD_RESOURCES_TEMPLATES_LIST, + "resources/templates/list"); +} + +TEST(MCP, method_notifications_initialized) { + EXPECT_EQ(sourcemeta::core::MCP_METHOD_NOTIFICATIONS_INITIALIZED, + "notifications/initialized"); +} + +TEST(MCP, code_resource_not_found) { + EXPECT_EQ(sourcemeta::core::MCP_CODE_RESOURCE_NOT_FOUND, + static_cast(-32002)); +} + +TEST(MCP, code_url_elicitation_required) { + EXPECT_EQ(sourcemeta::core::MCP_CODE_URL_ELICITATION_REQUIRED, + static_cast(-32042)); +} + +TEST(MCP, is_request_method_initialize) { + EXPECT_TRUE(sourcemeta::core::mcp_is_request_method("initialize")); +} + +TEST(MCP, is_request_method_ping) { + EXPECT_TRUE(sourcemeta::core::mcp_is_request_method("ping")); +} + +TEST(MCP, is_request_method_tools_list) { + EXPECT_TRUE(sourcemeta::core::mcp_is_request_method("tools/list")); +} + +TEST(MCP, is_request_method_tools_call) { + EXPECT_TRUE(sourcemeta::core::mcp_is_request_method("tools/call")); +} + +TEST(MCP, is_request_method_resources_list) { + EXPECT_TRUE(sourcemeta::core::mcp_is_request_method("resources/list")); +} + +TEST(MCP, is_request_method_resources_read) { + EXPECT_TRUE(sourcemeta::core::mcp_is_request_method("resources/read")); +} + +TEST(MCP, is_request_method_resources_templates_list) { + EXPECT_TRUE( + sourcemeta::core::mcp_is_request_method("resources/templates/list")); +} + +TEST(MCP, is_request_method_notifications_initialized_is_not_request) { + EXPECT_FALSE( + sourcemeta::core::mcp_is_request_method("notifications/initialized")); +} + +TEST(MCP, is_request_method_unknown) { + EXPECT_FALSE(sourcemeta::core::mcp_is_request_method("foo/bar")); +} + +TEST(MCP, is_request_method_empty) { + EXPECT_FALSE(sourcemeta::core::mcp_is_request_method("")); +} + +TEST(MCP, resolve_protocol_version_empty_defaults_to_2025_03_26) { + const auto result{sourcemeta::core::mcp_resolve_protocol_version("")}; + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), sourcemeta::core::MCPProtocolVersion::V_2025_03_26); +} + +TEST(MCP, resolve_protocol_version_2025_03_26) { + const auto result{ + sourcemeta::core::mcp_resolve_protocol_version("2025-03-26")}; + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), sourcemeta::core::MCPProtocolVersion::V_2025_03_26); +} + +TEST(MCP, resolve_protocol_version_2025_06_18) { + const auto result{ + sourcemeta::core::mcp_resolve_protocol_version("2025-06-18")}; + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), sourcemeta::core::MCPProtocolVersion::V_2025_06_18); +} + +TEST(MCP, resolve_protocol_version_2025_11_25) { + const auto result{ + sourcemeta::core::mcp_resolve_protocol_version("2025-11-25")}; + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), sourcemeta::core::MCPProtocolVersion::V_2025_11_25); +} + +TEST(MCP, resolve_protocol_version_unknown) { + EXPECT_FALSE( + sourcemeta::core::mcp_resolve_protocol_version("9999-01-01").has_value()); +} + +TEST(MCP, resolve_protocol_version_malformed) { + EXPECT_FALSE( + sourcemeta::core::mcp_resolve_protocol_version("not-a-date").has_value()); +} + +TEST(MCP, supports_output_schema_2025_03_26) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_output_schema( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26)); +} + +TEST(MCP, supports_output_schema_2025_06_18) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_output_schema( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18)); +} + +TEST(MCP, supports_output_schema_2025_11_25) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_output_schema( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25)); +} + +TEST(MCP, supports_structured_content_2025_03_26) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_structured_content( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26)); +} + +TEST(MCP, supports_structured_content_2025_06_18) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_structured_content( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18)); +} + +TEST(MCP, supports_structured_content_2025_11_25) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_structured_content( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25)); +} + +TEST(MCP, supports_resource_link_content_2025_03_26) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_resource_link_content( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26)); +} + +TEST(MCP, supports_resource_link_content_2025_06_18) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_resource_link_content( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18)); +} + +TEST(MCP, supports_resource_link_content_2025_11_25) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_resource_link_content( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25)); +} + +TEST(MCP, supports_implementation_title_2025_03_26) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_implementation_title( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26)); +} + +TEST(MCP, supports_implementation_title_2025_06_18) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_implementation_title( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18)); +} + +TEST(MCP, supports_implementation_title_2025_11_25) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_implementation_title( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25)); +} + +TEST(MCP, supports_implementation_description_2025_03_26) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_implementation_description( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26)); +} + +TEST(MCP, supports_implementation_description_2025_06_18) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_implementation_description( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18)); +} + +TEST(MCP, supports_implementation_description_2025_11_25) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_implementation_description( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25)); +} + +TEST(MCP, supports_implementation_website_url_2025_03_26) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_implementation_website_url( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26)); +} + +TEST(MCP, supports_implementation_website_url_2025_06_18) { + EXPECT_FALSE(sourcemeta::core::mcp_supports_implementation_website_url( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18)); +} + +TEST(MCP, supports_implementation_website_url_2025_11_25) { + EXPECT_TRUE(sourcemeta::core::mcp_supports_implementation_website_url( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25)); +} + +TEST(MCP, make_text_block) { + const auto block{sourcemeta::core::mcp_make_text_block("hello")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "text", + "text": "hello" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_text_block_empty_string) { + const auto block{sourcemeta::core::mcp_make_text_block("")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "text", + "text": "" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_text_block_with_newlines) { + const auto block{sourcemeta::core::mcp_make_text_block("line1\nline2")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "text", + "text": "line1\nline2" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_resource_link_2025_11_25_full) { + const auto block{sourcemeta::core::mcp_make_resource_link( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, "file:///foo", + "text/plain", "My File", "A description")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "resource_link", + "uri": "file:///foo", + "name": "My File", + "description": "A description", + "mimeType": "text/plain" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_resource_link_2025_11_25_without_name_and_description) { + const auto block{sourcemeta::core::mcp_make_resource_link( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, "file:///foo", + "text/plain")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "resource_link", + "uri": "file:///foo", + "mimeType": "text/plain" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_resource_link_2025_06_18_supports_structured) { + const auto block{sourcemeta::core::mcp_make_resource_link( + sourcemeta::core::MCPProtocolVersion::V_2025_06_18, "file:///foo", + "text/plain", "My File")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "resource_link", + "uri": "file:///foo", + "name": "My File", + "mimeType": "text/plain" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST( + MCP, + make_resource_link_2025_03_26_falls_back_to_text_with_name_and_description) { + const auto block{sourcemeta::core::mcp_make_resource_link( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, "file:///foo", + "text/plain", "My File", "A description")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "text", + "text": "My File (A description): file:///foo" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_resource_link_2025_03_26_falls_back_to_text_name_only) { + const auto block{sourcemeta::core::mcp_make_resource_link( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, "file:///foo", + "text/plain", "My File")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "text", + "text": "My File: file:///foo" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_resource_link_2025_03_26_falls_back_to_text_description_only) { + const auto block{sourcemeta::core::mcp_make_resource_link( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, "file:///foo", + "text/plain", {}, "A description")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "text", + "text": "A description: file:///foo" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, make_resource_link_2025_03_26_falls_back_to_text_uri_only) { + const auto block{sourcemeta::core::mcp_make_resource_link( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, "file:///foo", + "text/plain")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "type": "text", + "text": "file:///foo" + })JSON")}; + EXPECT_EQ(block, expected); +} + +TEST(MCP, tool_success_with_object_result) { + const auto identifier{sourcemeta::core::JSON{1}}; + auto result{sourcemeta::core::JSON::make_object()}; + result.assign("foo", sourcemeta::core::JSON{42}); + const auto envelope{sourcemeta::core::mcp_make_tool_success( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, identifier, + std::move(result))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [ + { "type": "text", "text": "{\n \"foo\": 42\n}" } + ], + "structuredContent": { "foo": 42 }, + "isError": false + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_success_with_array_result) { + const auto identifier{sourcemeta::core::JSON{"abc"}}; + const auto envelope{sourcemeta::core::mcp_make_tool_success( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, identifier, + sourcemeta::core::parse_json(R"([ 1, 2, 3 ])"))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": "abc", + "result": { + "content": [ + { "type": "text", "text": "[ 1, 2, 3 ]" } + ], + "structuredContent": [ 1, 2, 3 ], + "isError": false + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_success_with_null_id) { + const auto envelope{sourcemeta::core::mcp_make_tool_success( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, + sourcemeta::core::JSON{nullptr}, sourcemeta::core::JSON::make_object())}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": null, + "result": { + "content": [ + { "type": "text", "text": "{}" } + ], + "structuredContent": {}, + "isError": false + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_success_2025_03_26_omits_structured_content) { + const auto identifier{sourcemeta::core::JSON{1}}; + auto result{sourcemeta::core::JSON::make_object()}; + result.assign("foo", sourcemeta::core::JSON{42}); + const auto envelope{sourcemeta::core::mcp_make_tool_success( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, identifier, + std::move(result))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [ + { "type": "text", "text": "{\n \"foo\": 42\n}" } + ], + "isError": false + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_success_with_explicit_content_blocks) { + const auto identifier{sourcemeta::core::JSON{1}}; + auto structured{sourcemeta::core::JSON::make_object()}; + structured.assign("ok", sourcemeta::core::JSON{true}); + auto blocks{sourcemeta::core::JSON::make_array()}; + blocks.push_back(sourcemeta::core::mcp_make_text_block("done")); + const auto envelope{sourcemeta::core::mcp_make_tool_success( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, identifier, + std::move(structured), std::move(blocks))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [ { "type": "text", "text": "done" } ], + "structuredContent": { "ok": true }, + "isError": false + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_success_with_explicit_blocks_omits_structured_on_2025_03_26) { + const auto identifier{sourcemeta::core::JSON{1}}; + auto structured{sourcemeta::core::JSON::make_object()}; + structured.assign("ok", sourcemeta::core::JSON{true}); + auto blocks{sourcemeta::core::JSON::make_array()}; + blocks.push_back(sourcemeta::core::mcp_make_text_block("done")); + const auto envelope{sourcemeta::core::mcp_make_tool_success( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, identifier, + std::move(structured), std::move(blocks))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [ { "type": "text", "text": "done" } ], + "isError": false + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_error_with_message) { + const auto identifier{sourcemeta::core::JSON{7}}; + const auto envelope{ + sourcemeta::core::mcp_make_tool_error(identifier, "Schema not found")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 7, + "result": { + "content": [ { "type": "text", "text": "Schema not found" } ], + "isError": true + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_error_with_string_id) { + const auto identifier{sourcemeta::core::JSON{"req-1"}}; + const auto envelope{ + sourcemeta::core::mcp_make_tool_error(identifier, "Invalid input")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": "req-1", + "result": { + "content": [ { "type": "text", "text": "Invalid input" } ], + "isError": true + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, tool_error_with_null_id) { + const auto envelope{sourcemeta::core::mcp_make_tool_error( + sourcemeta::core::JSON{nullptr}, "Boom")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": null, + "result": { + "content": [ { "type": "text", "text": "Boom" } ], + "isError": true + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, error_resource_not_found_with_integer_id) { + const auto identifier{sourcemeta::core::JSON{3}}; + const auto envelope{sourcemeta::core::mcp_make_error_resource_not_found( + identifier, "file:///missing")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 3, + "error": { + "code": -32002, + "message": "Resource not found", + "data": "file:///missing" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, error_resource_not_found_with_string_id) { + const auto identifier{sourcemeta::core::JSON{"req-7"}}; + const auto envelope{sourcemeta::core::mcp_make_error_resource_not_found( + identifier, "file:///missing")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": "req-7", + "error": { + "code": -32002, + "message": "Resource not found", + "data": "file:///missing" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, make_resource_full) { + const auto resource{sourcemeta::core::mcp_make_resource( + "file:///a", "Alpha", "text/plain", "First file", + std::optional{1024})}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "uri": "file:///a", + "name": "Alpha", + "description": "First file", + "mimeType": "text/plain", + "size": 1024 + })JSON")}; + EXPECT_EQ(resource, expected); +} + +TEST(MCP, make_resource_without_description) { + const auto resource{ + sourcemeta::core::mcp_make_resource("file:///a", "Alpha", "text/plain")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "uri": "file:///a", + "name": "Alpha", + "mimeType": "text/plain" + })JSON")}; + EXPECT_EQ(resource, expected); +} + +TEST(MCP, make_resource_without_size) { + const auto resource{sourcemeta::core::mcp_make_resource( + "file:///a", "Alpha", "text/plain", "First file")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "uri": "file:///a", + "name": "Alpha", + "description": "First file", + "mimeType": "text/plain" + })JSON")}; + EXPECT_EQ(resource, expected); +} + +TEST(MCP, make_resource_text_content) { + const auto content{sourcemeta::core::mcp_make_resource_text_content( + "file:///a", "text/plain", "Hello")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "uri": "file:///a", + "mimeType": "text/plain", + "text": "Hello" + })JSON")}; + EXPECT_EQ(content, expected); +} + +TEST(MCP, make_resources_read_result_single) { + auto contents{sourcemeta::core::JSON::make_array()}; + contents.push_back(sourcemeta::core::mcp_make_resource_text_content( + "file:///a", "text/plain", "Hello")); + const auto result{ + sourcemeta::core::mcp_make_resources_read_result(std::move(contents))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "contents": [ + { "uri": "file:///a", "mimeType": "text/plain", "text": "Hello" } + ] + })JSON")}; + EXPECT_EQ(result, expected); +} + +TEST(MCP, make_resources_read_result_empty) { + const auto result{sourcemeta::core::mcp_make_resources_read_result( + sourcemeta::core::JSON::make_array())}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "contents": [] + })JSON")}; + EXPECT_EQ(result, expected); +} + +TEST(MCP, make_resource_template) { + const auto entry{sourcemeta::core::mcp_make_resource_template( + "file:///{path}", "Files", "Resolves a file path", "text/plain")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "uriTemplate": "file:///{path}", + "name": "Files", + "description": "Resolves a file path", + "mimeType": "text/plain" + })JSON")}; + EXPECT_EQ(entry, expected); +} + +TEST(MCP, make_tool_descriptor_default_annotations_2025_11_25) { + const auto entry{sourcemeta::core::mcp_make_tool_descriptor( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, "say", "Says hello", + sourcemeta::core::parse_json(R"({ "type": "object" })"))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "name": "say", + "description": "Says hello", + "inputSchema": { "type": "object" }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + })JSON")}; + EXPECT_EQ(entry, expected); +} + +TEST(MCP, make_tool_descriptor_with_output_schema_2025_11_25) { + const auto entry{sourcemeta::core::mcp_make_tool_descriptor( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, "say", "Says hello", + sourcemeta::core::parse_json(R"({ "type": "object" })"), + sourcemeta::core::parse_json(R"({ "type": "string" })"))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "name": "say", + "description": "Says hello", + "inputSchema": { "type": "object" }, + "outputSchema": { "type": "string" }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + })JSON")}; + EXPECT_EQ(entry, expected); +} + +TEST(MCP, make_tool_descriptor_with_output_schema_dropped_on_2025_03_26) { + const auto entry{sourcemeta::core::mcp_make_tool_descriptor( + sourcemeta::core::MCPProtocolVersion::V_2025_03_26, "say", "Says hello", + sourcemeta::core::parse_json(R"({ "type": "object" })"), + sourcemeta::core::parse_json(R"({ "type": "string" })"))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "name": "say", + "description": "Says hello", + "inputSchema": { "type": "object" }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + })JSON")}; + EXPECT_EQ(entry, expected); +} + +TEST(MCP, make_tool_descriptor_with_title_and_read_only_hints) { + sourcemeta::core::MCPToolAnnotations annotations; + annotations.title = "Say Hello"; + annotations.read_only = true; + annotations.destructive = false; + annotations.idempotent = true; + annotations.open_world = false; + const auto entry{sourcemeta::core::mcp_make_tool_descriptor( + sourcemeta::core::MCPProtocolVersion::V_2025_11_25, "say", "Says hello", + sourcemeta::core::parse_json(R"({ "type": "object" })"), std::nullopt, + annotations)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "name": "say", + "description": "Says hello", + "inputSchema": { "type": "object" }, + "annotations": { + "title": "Say Hello", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + } + })JSON")}; + EXPECT_EQ(entry, expected); +} + +TEST(MCP, make_initialize_result_minimal_2025_11_25) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": "2025-11-25" } + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", {}, {}, {}}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2025-11-25", + "capabilities": {}, + "serverInfo": { "name": "srv", "version": "1.0.0" } + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, make_initialize_result_with_all_capabilities) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": "2025-11-25" } + })JSON")}; + sourcemeta::core::MCPServerCapabilities capabilities; + capabilities.prompts = true; + capabilities.resources = true; + capabilities.tools = true; + capabilities.logging = true; + capabilities.completions = true; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", {}, {}, {}}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2025-11-25", + "capabilities": { + "prompts": {}, + "resources": {}, + "tools": {}, + "logging": {}, + "completions": {} + }, + "serverInfo": { "name": "srv", "version": "1.0.0" } + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(MCP, make_initialize_result_includes_instructions_when_provided) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": "2025-11-25" } + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", {}, {}, {}}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server, "Be careful.")}; + EXPECT_EQ(envelope.at("result").at("instructions").to_string(), + "Be careful."); +} + +TEST(MCP, make_initialize_result_includes_title_on_2025_06_18) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": "2025-06-18" } + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", "Server", + "desc", "https://x"}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + EXPECT_EQ(envelope.at("result").at("serverInfo").at("title").to_string(), + "Server"); + EXPECT_FALSE(envelope.at("result").at("serverInfo").defines("description")); + EXPECT_FALSE(envelope.at("result").at("serverInfo").defines("websiteUrl")); +} + +TEST(MCP, make_initialize_result_includes_full_implementation_on_2025_11_25) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": "2025-11-25" } + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", "Server", + "desc", "https://x"}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + EXPECT_EQ(envelope.at("result").at("serverInfo").at("title").to_string(), + "Server"); + EXPECT_EQ( + envelope.at("result").at("serverInfo").at("description").to_string(), + "desc"); + EXPECT_EQ(envelope.at("result").at("serverInfo").at("websiteUrl").to_string(), + "https://x"); +} + +TEST( + MCP, + make_initialize_result_strips_unsupported_implementation_fields_on_2025_03_26) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": "2025-03-26" } + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", "Server", + "desc", "https://x"}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + EXPECT_FALSE(envelope.at("result").at("serverInfo").defines("title")); + EXPECT_FALSE(envelope.at("result").at("serverInfo").defines("description")); + EXPECT_FALSE(envelope.at("result").at("serverInfo").defines("websiteUrl")); +} + +TEST(MCP, make_initialize_result_falls_back_to_2025_11_25_on_unknown_version) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { "protocolVersion": "9999-01-01" } + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", {}, {}, {}}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + EXPECT_EQ(envelope.at("result").at("protocolVersion").to_string(), + "2025-11-25"); +} + +TEST(MCP, make_initialize_result_returns_invalid_request_when_missing_params) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", "id": 1, "method": "initialize" + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", {}, {}, {}}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + EXPECT_EQ(envelope.at("error").at("code").to_integer(), + sourcemeta::core::JSONRPC_CODE_INVALID_REQUEST); +} + +TEST(MCP, make_initialize_result_returns_invalid_request_when_id_missing) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", "method": "initialize", "params": {} + })JSON")}; + const sourcemeta::core::MCPServerCapabilities capabilities; + const sourcemeta::core::MCPImplementation server{"srv", "1.0.0", {}, {}, {}}; + const auto envelope{sourcemeta::core::mcp_make_initialize_result( + request, capabilities, server)}; + EXPECT_EQ(envelope.at("error").at("code").to_integer(), + sourcemeta::core::JSONRPC_CODE_INVALID_REQUEST); +} + +TEST(MCP, tool_call_arguments_present) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { "name": "foo", "arguments": { "x": 1 } } + })JSON")}; + const auto *arguments{sourcemeta::core::mcp_tool_call_arguments(envelope)}; + ASSERT_NE(arguments, nullptr); + EXPECT_TRUE(arguments->is_object()); + EXPECT_EQ(arguments->at("x").to_integer(), 1); +} + +TEST(MCP, tool_call_arguments_missing) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { "name": "foo" } + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_tool_call_arguments(envelope), nullptr); +} + +TEST(MCP, tool_call_arguments_params_not_object) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": [ 1, 2 ] + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_tool_call_arguments(envelope), nullptr); +} + +TEST(MCP, tool_call_arguments_no_params) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call" + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_tool_call_arguments(envelope), nullptr); +} diff --git a/test/packaging/find_package/CMakeLists.txt b/test/packaging/find_package/CMakeLists.txt index b98fbdfc3..d252f5f92 100644 --- a/test/packaging/find_package/CMakeLists.txt +++ b/test/packaging/find_package/CMakeLists.txt @@ -25,4 +25,5 @@ target_link_libraries(core_hello PRIVATE sourcemeta::core::markdown) target_link_libraries(core_hello PRIVATE sourcemeta::core::options) target_link_libraries(core_hello PRIVATE sourcemeta::core::preprocessor) target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonrpc) +target_link_libraries(core_hello PRIVATE sourcemeta::core::mcp) target_link_libraries(core_hello PRIVATE sourcemeta::core::text) diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index 4c209fc1f..f430da285 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include From 87f49094cba9360fbf99f6201790ffeeec6f9ae0 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 19:57:18 -0400 Subject: [PATCH 2/6] Simpler Signed-off-by: Juan Cruz Viotti --- src/core/jsonrpc/jsonrpc.cc | 26 +++--- src/core/mcp/mcp.cc | 159 +++++++++++++++--------------------- 2 files changed, 75 insertions(+), 110 deletions(-) diff --git a/src/core/jsonrpc/jsonrpc.cc b/src/core/jsonrpc/jsonrpc.cc index 1217dec8d..0987f925d 100644 --- a/src/core/jsonrpc/jsonrpc.cc +++ b/src/core/jsonrpc/jsonrpc.cc @@ -113,13 +113,11 @@ auto jsonrpc_make_success(const sourcemeta::core::JSON &identifier, sourcemeta::core::JSON result) -> sourcemeta::core::JSON { auto envelope{sourcemeta::core::JSON::make_object()}; - envelope.assign_assume_new(std::string{"jsonrpc"}, - sourcemeta::core::JSON{"2.0"}, + envelope.assign_assume_new("jsonrpc", sourcemeta::core::JSON{"2.0"}, JSONRPC_HASH_JSONRPC); - envelope.assign_assume_new( - std::string{"id"}, sourcemeta::core::JSON{identifier}, JSONRPC_HASH_ID); - envelope.assign_assume_new(std::string{"result"}, std::move(result), - JSONRPC_HASH_RESULT); + envelope.assign_assume_new("id", sourcemeta::core::JSON{identifier}, + JSONRPC_HASH_ID); + envelope.assign_assume_new("result", std::move(result), JSONRPC_HASH_RESULT); return envelope; } @@ -134,26 +132,22 @@ auto jsonrpc_make_error(const sourcemeta::core::JSON *identifier, std::optional data) -> sourcemeta::core::JSON { auto envelope{sourcemeta::core::JSON::make_object()}; - envelope.assign_assume_new(std::string{"jsonrpc"}, - sourcemeta::core::JSON{"2.0"}, + envelope.assign_assume_new("jsonrpc", sourcemeta::core::JSON{"2.0"}, JSONRPC_HASH_JSONRPC); - envelope.assign_assume_new(std::string{"id"}, + envelope.assign_assume_new("id", identifier != nullptr ? sourcemeta::core::JSON{*identifier} : sourcemeta::core::JSON{nullptr}, JSONRPC_HASH_ID); auto error{sourcemeta::core::JSON::make_object()}; - error.assign_assume_new(std::string{"code"}, sourcemeta::core::JSON{code}, + error.assign_assume_new("code", sourcemeta::core::JSON{code}, JSONRPC_HASH_CODE); - error.assign_assume_new(std::string{"message"}, - sourcemeta::core::JSON{message}, + error.assign_assume_new("message", sourcemeta::core::JSON{message}, JSONRPC_HASH_MESSAGE); if (data.has_value()) { - error.assign_assume_new(std::string{"data"}, std::move(data.value()), - JSONRPC_HASH_DATA); + error.assign_assume_new("data", std::move(data.value()), JSONRPC_HASH_DATA); } - envelope.assign_assume_new(std::string{"error"}, std::move(error), - JSONRPC_HASH_ERROR); + envelope.assign_assume_new("error", std::move(error), JSONRPC_HASH_ERROR); return envelope; } diff --git a/src/core/mcp/mcp.cc b/src/core/mcp/mcp.cc index d05f67874..eb1b514bb 100644 --- a/src/core/mcp/mcp.cc +++ b/src/core/mcp/mcp.cc @@ -71,10 +71,9 @@ namespace sourcemeta::core { auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON { auto block{sourcemeta::core::JSON::make_object()}; - block.assign_assume_new(std::string{"type"}, sourcemeta::core::JSON{"text"}, + block.assign_assume_new("type", sourcemeta::core::JSON{"text"}, MCP_HASH_TYPE); - block.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}, - MCP_HASH_TEXT); + block.assign_assume_new("text", sourcemeta::core::JSON{text}, MCP_HASH_TEXT); return block; } @@ -103,22 +102,18 @@ auto mcp_make_resource_link(const MCPProtocolVersion version, } auto block{sourcemeta::core::JSON::make_object()}; - block.assign_assume_new(std::string{"type"}, - sourcemeta::core::JSON{"resource_link"}, + block.assign_assume_new("type", sourcemeta::core::JSON{"resource_link"}, MCP_HASH_TYPE); - block.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, - MCP_HASH_URI); + block.assign_assume_new("uri", sourcemeta::core::JSON{uri}, MCP_HASH_URI); if (!name.empty()) { - block.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + block.assign_assume_new("name", sourcemeta::core::JSON{name}, MCP_HASH_NAME); } if (!description.empty()) { - block.assign_assume_new(std::string{"description"}, - sourcemeta::core::JSON{description}, + block.assign_assume_new("description", sourcemeta::core::JSON{description}, MCP_HASH_DESCRIPTION); } - block.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}, + block.assign_assume_new("mimeType", sourcemeta::core::JSON{mime_type}, MCP_HASH_MIME_TYPE); return block; } @@ -134,15 +129,14 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, content.push_back(mcp_make_text_block(payload.str())); auto envelope_result{sourcemeta::core::JSON::make_object()}; - envelope_result.assign_assume_new(std::string{"content"}, std::move(content), + envelope_result.assign_assume_new("content", std::move(content), MCP_HASH_CONTENT); if (mcp_supports_structured_content(version)) { - envelope_result.assign_assume_new(std::string{"structuredContent"}, - std::move(result), + envelope_result.assign_assume_new("structuredContent", std::move(result), MCP_HASH_STRUCTURED_CONTENT); } - envelope_result.assign_assume_new( - std::string{"isError"}, sourcemeta::core::JSON{false}, MCP_HASH_IS_ERROR); + envelope_result.assign_assume_new("isError", sourcemeta::core::JSON{false}, + MCP_HASH_IS_ERROR); return sourcemeta::core::jsonrpc_make_success(identifier, std::move(envelope_result)); } @@ -153,15 +147,15 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, sourcemeta::core::JSON content_blocks) -> sourcemeta::core::JSON { auto envelope_result{sourcemeta::core::JSON::make_object()}; - envelope_result.assign_assume_new( - std::string{"content"}, std::move(content_blocks), MCP_HASH_CONTENT); + envelope_result.assign_assume_new("content", std::move(content_blocks), + MCP_HASH_CONTENT); if (mcp_supports_structured_content(version)) { - envelope_result.assign_assume_new(std::string{"structuredContent"}, + envelope_result.assign_assume_new("structuredContent", std::move(structured), MCP_HASH_STRUCTURED_CONTENT); } - envelope_result.assign_assume_new( - std::string{"isError"}, sourcemeta::core::JSON{false}, MCP_HASH_IS_ERROR); + envelope_result.assign_assume_new("isError", sourcemeta::core::JSON{false}, + MCP_HASH_IS_ERROR); return sourcemeta::core::jsonrpc_make_success(identifier, std::move(envelope_result)); } @@ -173,10 +167,10 @@ auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, content.push_back(mcp_make_text_block(message)); auto envelope_result{sourcemeta::core::JSON::make_object()}; - envelope_result.assign_assume_new(std::string{"content"}, std::move(content), + envelope_result.assign_assume_new("content", std::move(content), MCP_HASH_CONTENT); - envelope_result.assign_assume_new( - std::string{"isError"}, sourcemeta::core::JSON{true}, MCP_HASH_IS_ERROR); + envelope_result.assign_assume_new("isError", sourcemeta::core::JSON{true}, + MCP_HASH_IS_ERROR); return sourcemeta::core::jsonrpc_make_success(identifier, std::move(envelope_result)); } @@ -195,21 +189,18 @@ auto mcp_make_resource(const std::string_view uri, const std::string_view name, const std::optional size) -> sourcemeta::core::JSON { auto resource{sourcemeta::core::JSON::make_object()}; - resource.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, - MCP_HASH_URI); - resource.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, + resource.assign_assume_new("uri", sourcemeta::core::JSON{uri}, MCP_HASH_URI); + resource.assign_assume_new("name", sourcemeta::core::JSON{name}, MCP_HASH_NAME); if (!description.empty()) { - resource.assign_assume_new(std::string{"description"}, + resource.assign_assume_new("description", sourcemeta::core::JSON{description}, MCP_HASH_DESCRIPTION); } - resource.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}, + resource.assign_assume_new("mimeType", sourcemeta::core::JSON{mime_type}, MCP_HASH_MIME_TYPE); if (size.has_value()) { - resource.assign_assume_new(std::string{"size"}, - sourcemeta::core::JSON{size.value()}, + resource.assign_assume_new("size", sourcemeta::core::JSON{size.value()}, MCP_HASH_SIZE); } return resource; @@ -220,21 +211,17 @@ auto mcp_make_resource_text_content(const std::string_view uri, const std::string_view text) -> sourcemeta::core::JSON { auto entry{sourcemeta::core::JSON::make_object()}; - entry.assign_assume_new(std::string{"uri"}, sourcemeta::core::JSON{uri}, - MCP_HASH_URI); - entry.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}, + entry.assign_assume_new("uri", sourcemeta::core::JSON{uri}, MCP_HASH_URI); + entry.assign_assume_new("mimeType", sourcemeta::core::JSON{mime_type}, MCP_HASH_MIME_TYPE); - entry.assign_assume_new(std::string{"text"}, sourcemeta::core::JSON{text}, - MCP_HASH_TEXT); + entry.assign_assume_new("text", sourcemeta::core::JSON{text}, MCP_HASH_TEXT); return entry; } auto mcp_make_resources_read_result(sourcemeta::core::JSON contents) -> sourcemeta::core::JSON { auto result{sourcemeta::core::JSON::make_object()}; - result.assign_assume_new(std::string{"contents"}, std::move(contents), - MCP_HASH_CONTENTS); + result.assign_assume_new("contents", std::move(contents), MCP_HASH_CONTENTS); return result; } @@ -244,16 +231,12 @@ auto mcp_make_resource_template(const std::string_view uri_template, const std::string_view mime_type) -> sourcemeta::core::JSON { auto entry{sourcemeta::core::JSON::make_object()}; - entry.assign_assume_new(std::string{"uriTemplate"}, - sourcemeta::core::JSON{uri_template}, + entry.assign_assume_new("uriTemplate", sourcemeta::core::JSON{uri_template}, MCP_HASH_URI_TEMPLATE); - entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, - MCP_HASH_NAME); - entry.assign_assume_new(std::string{"description"}, - sourcemeta::core::JSON{description}, + entry.assign_assume_new("name", sourcemeta::core::JSON{name}, MCP_HASH_NAME); + entry.assign_assume_new("description", sourcemeta::core::JSON{description}, MCP_HASH_DESCRIPTION); - entry.assign_assume_new(std::string{"mimeType"}, - sourcemeta::core::JSON{mime_type}, + entry.assign_assume_new("mimeType", sourcemeta::core::JSON{mime_type}, MCP_HASH_MIME_TYPE); return entry; } @@ -267,40 +250,35 @@ auto mcp_make_tool_descriptor( assert(!annotations.read_only || annotations.idempotent); auto entry{sourcemeta::core::JSON::make_object()}; - entry.assign_assume_new(std::string{"name"}, sourcemeta::core::JSON{name}, - MCP_HASH_NAME); - entry.assign_assume_new(std::string{"description"}, - sourcemeta::core::JSON{description}, + entry.assign_assume_new("name", sourcemeta::core::JSON{name}, MCP_HASH_NAME); + entry.assign_assume_new("description", sourcemeta::core::JSON{description}, MCP_HASH_DESCRIPTION); - entry.assign_assume_new(std::string{"inputSchema"}, std::move(input_schema), + entry.assign_assume_new("inputSchema", std::move(input_schema), MCP_HASH_INPUT_SCHEMA); if (output_schema.has_value() && mcp_supports_output_schema(version)) { - entry.assign_assume_new(std::string{"outputSchema"}, - std::move(output_schema).value(), + entry.assign_assume_new("outputSchema", std::move(output_schema).value(), MCP_HASH_OUTPUT_SCHEMA); } auto annotations_object{sourcemeta::core::JSON::make_object()}; if (!annotations.title.empty()) { annotations_object.assign_assume_new( - std::string{"title"}, sourcemeta::core::JSON{annotations.title}, - MCP_HASH_TITLE); + "title", sourcemeta::core::JSON{annotations.title}, MCP_HASH_TITLE); } annotations_object.assign_assume_new( - std::string{"readOnlyHint"}, - sourcemeta::core::JSON{annotations.read_only}, MCP_HASH_READ_ONLY_HINT); + "readOnlyHint", sourcemeta::core::JSON{annotations.read_only}, + MCP_HASH_READ_ONLY_HINT); annotations_object.assign_assume_new( - std::string{"destructiveHint"}, - sourcemeta::core::JSON{annotations.destructive}, + "destructiveHint", sourcemeta::core::JSON{annotations.destructive}, MCP_HASH_DESTRUCTIVE_HINT); annotations_object.assign_assume_new( - std::string{"idempotentHint"}, - sourcemeta::core::JSON{annotations.idempotent}, MCP_HASH_IDEMPOTENT_HINT); + "idempotentHint", sourcemeta::core::JSON{annotations.idempotent}, + MCP_HASH_IDEMPOTENT_HINT); annotations_object.assign_assume_new( - std::string{"openWorldHint"}, - sourcemeta::core::JSON{annotations.open_world}, MCP_HASH_OPEN_WORLD_HINT); - entry.assign_assume_new(std::string{"annotations"}, - std::move(annotations_object), MCP_HASH_ANNOTATIONS); + "openWorldHint", sourcemeta::core::JSON{annotations.open_world}, + MCP_HASH_OPEN_WORLD_HINT); + entry.assign_assume_new("annotations", std::move(annotations_object), + MCP_HASH_ANNOTATIONS); return entry; } @@ -330,67 +308,60 @@ auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, auto capabilities_object{sourcemeta::core::JSON::make_object()}; if (capabilities.prompts) { - capabilities_object.assign_assume_new(std::string{"prompts"}, - sourcemeta::core::JSON::make_object(), - MCP_HASH_PROMPTS); + capabilities_object.assign_assume_new( + "prompts", sourcemeta::core::JSON::make_object(), MCP_HASH_PROMPTS); } if (capabilities.resources) { - capabilities_object.assign_assume_new(std::string{"resources"}, - sourcemeta::core::JSON::make_object(), - MCP_HASH_RESOURCES); + capabilities_object.assign_assume_new( + "resources", sourcemeta::core::JSON::make_object(), MCP_HASH_RESOURCES); } if (capabilities.tools) { - capabilities_object.assign_assume_new(std::string{"tools"}, - sourcemeta::core::JSON::make_object(), - MCP_HASH_TOOLS); + capabilities_object.assign_assume_new( + "tools", sourcemeta::core::JSON::make_object(), MCP_HASH_TOOLS); } if (capabilities.logging) { - capabilities_object.assign_assume_new(std::string{"logging"}, - sourcemeta::core::JSON::make_object(), - MCP_HASH_LOGGING); + capabilities_object.assign_assume_new( + "logging", sourcemeta::core::JSON::make_object(), MCP_HASH_LOGGING); } if (capabilities.completions) { - capabilities_object.assign_assume_new(std::string{"completions"}, + capabilities_object.assign_assume_new("completions", sourcemeta::core::JSON::make_object(), MCP_HASH_COMPLETIONS); } auto server_info{sourcemeta::core::JSON::make_object()}; + server_info.assign_assume_new("name", sourcemeta::core::JSON{server.name}, + MCP_HASH_NAME); server_info.assign_assume_new( - std::string{"name"}, sourcemeta::core::JSON{server.name}, MCP_HASH_NAME); - server_info.assign_assume_new(std::string{"version"}, - sourcemeta::core::JSON{server.version}, - MCP_HASH_VERSION); + "version", sourcemeta::core::JSON{server.version}, MCP_HASH_VERSION); if (!server.title.empty() && mcp_supports_implementation_title(version)) { - server_info.assign_assume_new(std::string{"title"}, - sourcemeta::core::JSON{server.title}, + server_info.assign_assume_new("title", sourcemeta::core::JSON{server.title}, MCP_HASH_TITLE); } if (!server.description.empty() && mcp_supports_implementation_description(version)) { - server_info.assign_assume_new(std::string{"description"}, + server_info.assign_assume_new("description", sourcemeta::core::JSON{server.description}, MCP_HASH_DESCRIPTION); } if (!server.website_url.empty() && mcp_supports_implementation_website_url(version)) { - server_info.assign_assume_new(std::string{"websiteUrl"}, + server_info.assign_assume_new("websiteUrl", sourcemeta::core::JSON{server.website_url}, MCP_HASH_WEBSITE_URL); } auto result{sourcemeta::core::JSON::make_object()}; result.assign_assume_new( - std::string{"protocolVersion"}, + "protocolVersion", sourcemeta::core::JSON{mcp_protocol_version_string(version)}, MCP_HASH_PROTOCOL_VERSION); - result.assign_assume_new(std::string{"capabilities"}, - std::move(capabilities_object), + result.assign_assume_new("capabilities", std::move(capabilities_object), MCP_HASH_CAPABILITIES); - result.assign_assume_new(std::string{"serverInfo"}, std::move(server_info), + result.assign_assume_new("serverInfo", std::move(server_info), MCP_HASH_SERVER_INFO); if (!instructions.empty()) { - result.assign_assume_new(std::string{"instructions"}, + result.assign_assume_new("instructions", sourcemeta::core::JSON{instructions}, MCP_HASH_INSTRUCTIONS); } From 40d50f1e804464be3bfeb16bdbf7f14bb1ea9d5c Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 20:01:08 -0400 Subject: [PATCH 3/6] Comment Signed-off-by: Juan Cruz Viotti --- src/core/mcp/include/sourcemeta/core/mcp.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/mcp/include/sourcemeta/core/mcp.h b/src/core/mcp/include/sourcemeta/core/mcp.h index 53bb2ca18..827ad480e 100644 --- a/src/core/mcp/include/sourcemeta/core/mcp.h +++ b/src/core/mcp/include/sourcemeta/core/mcp.h @@ -143,6 +143,10 @@ constexpr auto mcp_resolve_protocol_version(const std::string_view header) noexcept -> std::optional { if (header.empty()) { + // Per the MCP Streamable HTTP transport spec: if the server does not + // receive an MCP-Protocol-Version header, and has no other way to identify + // the version, the server SHOULD assume protocol version 2025-03-26. + // https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header return MCPProtocolVersion::V_2025_03_26; } if (header == "2025-11-25") { From d82ae1582d72d45b646b00fa1db7e44b784fd10d Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 20:03:44 -0400 Subject: [PATCH 4/6] More Signed-off-by: Juan Cruz Viotti --- src/core/mcp/include/sourcemeta/core/mcp.h | 5 ++-- src/core/mcp/mcp.cc | 20 ++++++++------- test/mcp/mcp_test.cc | 30 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/core/mcp/include/sourcemeta/core/mcp.h b/src/core/mcp/include/sourcemeta/core/mcp.h index 827ad480e..16a6e7a9e 100644 --- a/src/core/mcp/include/sourcemeta/core/mcp.h +++ b/src/core/mcp/include/sourcemeta/core/mcp.h @@ -523,9 +523,8 @@ auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, /// @ingroup mcp /// Borrow the `arguments` object from a JSON-RPC `tools/call` envelope, or -/// return `nullptr` when it is missing or `params` is not an object. The -/// returned pointer is valid for the lifetime of the input envelope. For -/// example: +/// return `nullptr` if no `arguments` object is present. The returned pointer +/// is valid for the lifetime of the input envelope. For example: /// /// ```cpp /// #include diff --git a/src/core/mcp/mcp.cc b/src/core/mcp/mcp.cc index eb1b514bb..452981d1a 100644 --- a/src/core/mcp/mcp.cc +++ b/src/core/mcp/mcp.cc @@ -296,12 +296,11 @@ auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, } std::string_view requested_version{}; - if (parameters->defines("protocolVersion", MCP_HASH_PROTOCOL_VERSION) && - parameters->at("protocolVersion", MCP_HASH_PROTOCOL_VERSION) - .is_string()) { - requested_version = - parameters->at("protocolVersion", MCP_HASH_PROTOCOL_VERSION) - .to_string(); + const auto *protocol_version_field{ + parameters->try_at("protocolVersion", MCP_HASH_PROTOCOL_VERSION)}; + if (protocol_version_field != nullptr && + protocol_version_field->is_string()) { + requested_version = protocol_version_field->to_string(); } const auto resolved{mcp_resolve_protocol_version(requested_version)}; const auto version{resolved.value_or(MCPProtocolVersion::V_2025_11_25)}; @@ -371,11 +370,14 @@ auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, auto mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) -> const sourcemeta::core::JSON * { const auto *parameters{sourcemeta::core::jsonrpc_params(envelope)}; - if (parameters == nullptr || !parameters->is_object() || - !parameters->defines("arguments", MCP_HASH_ARGUMENTS)) { + if (parameters == nullptr || !parameters->is_object()) { return nullptr; } - return ¶meters->at("arguments", MCP_HASH_ARGUMENTS); + const auto *arguments{parameters->try_at("arguments", MCP_HASH_ARGUMENTS)}; + if (arguments == nullptr || !arguments->is_object()) { + return nullptr; + } + return arguments; } } // namespace sourcemeta::core diff --git a/test/mcp/mcp_test.cc b/test/mcp/mcp_test.cc index 8be8177ae..67d0fbf4f 100644 --- a/test/mcp/mcp_test.cc +++ b/test/mcp/mcp_test.cc @@ -922,3 +922,33 @@ TEST(MCP, tool_call_arguments_no_params) { })JSON")}; EXPECT_EQ(sourcemeta::core::mcp_tool_call_arguments(envelope), nullptr); } + +TEST(MCP, tool_call_arguments_string_value) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { "name": "foo", "arguments": "not-an-object" } + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_tool_call_arguments(envelope), nullptr); +} + +TEST(MCP, tool_call_arguments_array_value) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { "name": "foo", "arguments": [ 1, 2, 3 ] } + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_tool_call_arguments(envelope), nullptr); +} + +TEST(MCP, tool_call_arguments_null_value) { + const auto envelope{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { "name": "foo", "arguments": null } + })JSON")}; + EXPECT_EQ(sourcemeta::core::mcp_tool_call_arguments(envelope), nullptr); +} From 60d67fa9731d5828ce37fde39f8880dd8600f3f4 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 20:06:54 -0400 Subject: [PATCH 5/6] Simpler Signed-off-by: Juan Cruz Viotti --- src/core/mcp/include/sourcemeta/core/mcp.h | 54 ++++++++-------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/src/core/mcp/include/sourcemeta/core/mcp.h b/src/core/mcp/include/sourcemeta/core/mcp.h index 16a6e7a9e..e87b7acd1 100644 --- a/src/core/mcp/include/sourcemeta/core/mcp.h +++ b/src/core/mcp/include/sourcemeta/core/mcp.h @@ -15,8 +15,7 @@ #include // std::unreachable /// @defgroup mcp MCP -/// @brief Helpers for building Model Context Protocol (MCP) envelopes on top of -/// JSON-RPC 2.0. +/// @brief Helpers for building Model Context Protocol (MCP) envelopes. /// /// This functionality is included as follows: /// @@ -125,9 +124,7 @@ constexpr auto mcp_is_request_method(const std::string_view method) noexcept /// @ingroup mcp /// Resolve an `MCP-Protocol-Version` header value into a known protocol -/// version. An empty header defaults to the earliest revision, per the MCP -/// specification. Returns `std::nullopt` when the header is unrecognised. For -/// example: +/// version, or `std::nullopt` when the value is unrecognised. For example: /// /// ```cpp /// #include @@ -226,9 +223,7 @@ SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON; /// @ingroup mcp -/// Build an MCP `resource_link` content block, or a `text` content block -/// fallback when the protocol version does not support the link form. For -/// example: +/// Build an MCP content block referencing a resource by URI. For example: /// /// ```cpp /// #include @@ -248,10 +243,8 @@ auto mcp_make_resource_link(const MCPProtocolVersion version, -> sourcemeta::core::JSON; /// @ingroup mcp -/// Build a JSON-RPC envelope wrapping a successful MCP tool call response. The -/// `result` is serialised as a `text` content block and additionally copied -/// into `structuredContent` when the protocol version supports it. For -/// example: +/// Build a JSON-RPC envelope wrapping a successful MCP tool call response from +/// the given result payload. For example: /// /// ```cpp /// #include @@ -274,10 +267,8 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, -> sourcemeta::core::JSON; /// @ingroup mcp -/// Build a JSON-RPC envelope wrapping a successful MCP tool call response with -/// caller-provided content blocks and structured payload. The structured -/// payload is included only when the protocol version supports it. For -/// example: +/// Build a JSON-RPC envelope wrapping a successful MCP tool call response from +/// caller-provided content blocks and a structured payload. For example: /// /// ```cpp /// #include @@ -303,9 +294,8 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, -> sourcemeta::core::JSON; /// @ingroup mcp -/// Build a JSON-RPC envelope wrapping a failed MCP tool call response. The -/// `isError` field is set to `true` and `content` carries a single `text` -/// block. For example: +/// Build a JSON-RPC envelope wrapping a failed MCP tool call response with the +/// given error message. For example: /// /// ```cpp /// #include @@ -323,8 +313,8 @@ auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, -> sourcemeta::core::JSON; /// @ingroup mcp -/// Build a JSON-RPC error envelope using the MCP "resource not found" code -/// and the offending URI as the `data` field. For example: +/// Build a JSON-RPC error envelope reporting that an MCP resource URI could +/// not be resolved. For example: /// /// ```cpp /// #include @@ -342,9 +332,8 @@ auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &identifier, -> sourcemeta::core::JSON; /// @ingroup mcp -/// Build an MCP resource descriptor as used in `resources/list` responses. -/// The `description` is omitted when empty and the `size` is omitted when -/// unset. For example: +/// Build an MCP resource descriptor as used in `resources/list` responses. For +/// example: /// /// ```cpp /// #include @@ -419,9 +408,7 @@ auto mcp_make_resource_template(const std::string_view uri_template, -> sourcemeta::core::JSON; /// @ingroup mcp -/// Optional hints attached to an MCP tool descriptor. Field semantics follow -/// the MCP specification: defaults reflect the worst-case assumptions (a -/// destructive, open-world, non-idempotent tool). +/// Optional hints attached to an MCP tool descriptor. struct MCPToolAnnotations { /// Optional human-readable title for the tool. std::string_view title = {}; @@ -437,9 +424,7 @@ struct MCPToolAnnotations { }; /// @ingroup mcp -/// Build a single entry for an MCP `tools/list` response. The `outputSchema` -/// is dropped automatically when the protocol version predates its support. -/// For example: +/// Build a single entry for an MCP `tools/list` response. For example: /// /// ```cpp /// #include @@ -459,8 +444,7 @@ auto mcp_make_tool_descriptor( /// @ingroup mcp /// Implementation info advertised by an MCP server during the initialize -/// handshake. Fields that the negotiated protocol version does not support -/// are dropped automatically. +/// handshake. struct MCPImplementation { /// Short machine-readable server name. std::string_view name; @@ -476,8 +460,7 @@ struct MCPImplementation { /// @ingroup mcp /// Boolean toggles for the MCP `capabilities` object returned during the -/// initialize handshake. Each toggle gates the presence of an empty object -/// under the corresponding key. +/// initialize handshake. struct MCPServerCapabilities { /// Whether the server advertises prompts. bool prompts = false; @@ -493,8 +476,7 @@ struct MCPServerCapabilities { /// @ingroup mcp /// Build the JSON-RPC envelope returned in response to an MCP `initialize` -/// request. Returns a JSON-RPC invalid-request envelope when the incoming -/// request lacks an identifier or a `params` object. For example: +/// request. For example: /// /// ```cpp /// #include From d59dfbc1d35c553bc3b87f635c947fe794e7324a Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 20:10:47 -0400 Subject: [PATCH 6/6] Better Signed-off-by: Juan Cruz Viotti --- .../jsonrpc/include/sourcemeta/core/jsonrpc.h | 9 +- src/core/jsonrpc/jsonrpc.cc | 12 ++- src/core/mcp/include/sourcemeta/core/mcp.h | 83 +++++++++---------- src/core/mcp/mcp.cc | 55 ++++++------ 4 files changed, 77 insertions(+), 82 deletions(-) diff --git a/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h b/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h index 3ac86a8ae..314fafbcd 100644 --- a/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h +++ b/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h @@ -7,9 +7,8 @@ #include -#include // std::int64_t -#include // std::optional, std::nullopt -#include // std::string_view +#include // std::int64_t +#include // std::optional, std::nullopt /// @defgroup jsonrpc JSON-RPC /// @brief An implementation of the JSON-RPC 2.0 specification. @@ -118,7 +117,7 @@ auto jsonrpc_is_request(const sourcemeta::core::JSON &request) -> bool; /// assert(sourcemeta::core::jsonrpc_method(request) == "ping"); /// ``` SOURCEMETA_CORE_JSONRPC_EXPORT -auto jsonrpc_method(const sourcemeta::core::JSON &request) -> std::string_view; +auto jsonrpc_method(const sourcemeta::core::JSON &request) -> JSON::StringView; /// @ingroup jsonrpc /// Extract the params from a JSON-RPC 2.0 envelope, or `nullptr` when the @@ -218,7 +217,7 @@ auto jsonrpc_make_success_empty(const sourcemeta::core::JSON &identifier) /// ``` SOURCEMETA_CORE_JSONRPC_EXPORT auto jsonrpc_make_error(const sourcemeta::core::JSON *identifier, - const std::int64_t code, const std::string_view message, + const std::int64_t code, const JSON::StringView message, std::optional data = std::nullopt) -> sourcemeta::core::JSON; diff --git a/src/core/jsonrpc/jsonrpc.cc b/src/core/jsonrpc/jsonrpc.cc index 0987f925d..ca15a878e 100644 --- a/src/core/jsonrpc/jsonrpc.cc +++ b/src/core/jsonrpc/jsonrpc.cc @@ -2,11 +2,9 @@ #include -#include // std::int64_t -#include // std::optional, std::nullopt -#include // std::string -#include // std::string_view -#include // std::move +#include // std::int64_t +#include // std::optional, std::nullopt +#include // std::move namespace { @@ -66,7 +64,7 @@ auto jsonrpc_is_request(const sourcemeta::core::JSON &request) -> bool { return method_field != nullptr && method_field->is_string(); } -auto jsonrpc_method(const sourcemeta::core::JSON &request) -> std::string_view { +auto jsonrpc_method(const sourcemeta::core::JSON &request) -> JSON::StringView { if (!request.is_object()) { return {}; } @@ -128,7 +126,7 @@ auto jsonrpc_make_success_empty(const sourcemeta::core::JSON &identifier) } auto jsonrpc_make_error(const sourcemeta::core::JSON *identifier, - const std::int64_t code, const std::string_view message, + const std::int64_t code, const JSON::StringView message, std::optional data) -> sourcemeta::core::JSON { auto envelope{sourcemeta::core::JSON::make_object()}; diff --git a/src/core/mcp/include/sourcemeta/core/mcp.h b/src/core/mcp/include/sourcemeta/core/mcp.h index e87b7acd1..9870afcba 100644 --- a/src/core/mcp/include/sourcemeta/core/mcp.h +++ b/src/core/mcp/include/sourcemeta/core/mcp.h @@ -8,11 +8,10 @@ #include #include -#include // std::size_t -#include // std::int64_t, std::uint8_t -#include // std::optional, std::nullopt -#include // std::string_view -#include // std::unreachable +#include // std::size_t +#include // std::int64_t, std::uint8_t +#include // std::optional, std::nullopt +#include // std::unreachable /// @defgroup mcp MCP /// @brief Helpers for building Model Context Protocol (MCP) envelopes. @@ -47,7 +46,7 @@ enum class MCPProtocolVersion : std::uint8_t { /// ``` constexpr auto mcp_protocol_version_string(const MCPProtocolVersion version) noexcept - -> std::string_view { + -> JSON::StringView { switch (version) { case MCPProtocolVersion::V_2025_03_26: return "2025-03-26"; @@ -61,36 +60,36 @@ mcp_protocol_version_string(const MCPProtocolVersion version) noexcept /// @ingroup mcp /// The MCP method name for the `initialize` request. -constexpr std::string_view MCP_METHOD_INITIALIZE{"initialize"}; +constexpr JSON::StringView MCP_METHOD_INITIALIZE{"initialize"}; /// @ingroup mcp /// The MCP method name for the `ping` request. -constexpr std::string_view MCP_METHOD_PING{"ping"}; +constexpr JSON::StringView MCP_METHOD_PING{"ping"}; /// @ingroup mcp /// The MCP method name for the `tools/list` request. -constexpr std::string_view MCP_METHOD_TOOLS_LIST{"tools/list"}; +constexpr JSON::StringView MCP_METHOD_TOOLS_LIST{"tools/list"}; /// @ingroup mcp /// The MCP method name for the `tools/call` request. -constexpr std::string_view MCP_METHOD_TOOLS_CALL{"tools/call"}; +constexpr JSON::StringView MCP_METHOD_TOOLS_CALL{"tools/call"}; /// @ingroup mcp /// The MCP method name for the `resources/list` request. -constexpr std::string_view MCP_METHOD_RESOURCES_LIST{"resources/list"}; +constexpr JSON::StringView MCP_METHOD_RESOURCES_LIST{"resources/list"}; /// @ingroup mcp /// The MCP method name for the `resources/read` request. -constexpr std::string_view MCP_METHOD_RESOURCES_READ{"resources/read"}; +constexpr JSON::StringView MCP_METHOD_RESOURCES_READ{"resources/read"}; /// @ingroup mcp /// The MCP method name for the `resources/templates/list` request. -constexpr std::string_view MCP_METHOD_RESOURCES_TEMPLATES_LIST{ +constexpr JSON::StringView MCP_METHOD_RESOURCES_TEMPLATES_LIST{ "resources/templates/list"}; /// @ingroup mcp /// The MCP method name for the `notifications/initialized` notification. -constexpr std::string_view MCP_METHOD_NOTIFICATIONS_INITIALIZED{ +constexpr JSON::StringView MCP_METHOD_NOTIFICATIONS_INITIALIZED{ "notifications/initialized"}; /// @ingroup mcp @@ -113,7 +112,7 @@ constexpr std::int64_t MCP_CODE_URL_ELICITATION_REQUIRED{-32042}; /// assert(sourcemeta::core::mcp_is_request_method("initialize")); /// assert(!sourcemeta::core::mcp_is_request_method("notifications/initialized")); /// ``` -constexpr auto mcp_is_request_method(const std::string_view method) noexcept +constexpr auto mcp_is_request_method(const JSON::StringView method) noexcept -> bool { return method == MCP_METHOD_INITIALIZE || method == MCP_METHOD_PING || method == MCP_METHOD_TOOLS_LIST || method == MCP_METHOD_TOOLS_CALL || @@ -137,7 +136,7 @@ constexpr auto mcp_is_request_method(const std::string_view method) noexcept /// sourcemeta::core::MCPProtocolVersion::V_2025_11_25); /// ``` constexpr auto -mcp_resolve_protocol_version(const std::string_view header) noexcept +mcp_resolve_protocol_version(const JSON::StringView header) noexcept -> std::optional { if (header.empty()) { // Per the MCP Streamable HTTP transport spec: if the server does not @@ -220,7 +219,7 @@ constexpr auto mcp_supports_implementation_website_url( /// assert(block.at("text").to_string() == "hello"); /// ``` SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON; +auto mcp_make_text_block(const JSON::StringView text) -> sourcemeta::core::JSON; /// @ingroup mcp /// Build an MCP content block referencing a resource by URI. For example: @@ -236,10 +235,10 @@ auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON; /// ``` SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_resource_link(const MCPProtocolVersion version, - const std::string_view uri, - const std::string_view mime_type, - const std::string_view name = {}, - const std::string_view description = {}) + const JSON::StringView uri, + const JSON::StringView mime_type, + const JSON::StringView name = {}, + const JSON::StringView description = {}) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -309,7 +308,7 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, /// ``` SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, - const std::string_view message) + const JSON::StringView message) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -328,7 +327,7 @@ auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, /// ``` SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &identifier, - const std::string_view uri) + const JSON::StringView uri) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -344,9 +343,9 @@ auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &identifier, /// assert(resource.at("uri").to_string() == "file:///a"); /// ``` SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource(const std::string_view uri, const std::string_view name, - const std::string_view mime_type, - const std::string_view description = {}, +auto mcp_make_resource(const JSON::StringView uri, const JSON::StringView name, + const JSON::StringView mime_type, + const JSON::StringView description = {}, const std::optional size = std::nullopt) -> sourcemeta::core::JSON; @@ -362,9 +361,9 @@ auto mcp_make_resource(const std::string_view uri, const std::string_view name, /// assert(content.at("text").to_string() == "Hello"); /// ``` SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource_text_content(const std::string_view uri, - const std::string_view mime_type, - const std::string_view text) +auto mcp_make_resource_text_content(const JSON::StringView uri, + const JSON::StringView mime_type, + const JSON::StringView text) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -401,17 +400,17 @@ auto mcp_make_resources_read_result(sourcemeta::core::JSON contents) /// assert(entry.at("uriTemplate").to_string() == "file:///{path}"); /// ``` SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource_template(const std::string_view uri_template, - const std::string_view name, - const std::string_view description, - const std::string_view mime_type) +auto mcp_make_resource_template(const JSON::StringView uri_template, + const JSON::StringView name, + const JSON::StringView description, + const JSON::StringView mime_type) -> sourcemeta::core::JSON; /// @ingroup mcp /// Optional hints attached to an MCP tool descriptor. struct MCPToolAnnotations { /// Optional human-readable title for the tool. - std::string_view title = {}; + JSON::StringView title = {}; /// `true` when the tool guarantees no side effects. bool read_only = false; /// `true` when the tool may mutate or delete state. @@ -437,8 +436,8 @@ struct MCPToolAnnotations { /// ``` SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_tool_descriptor( - const MCPProtocolVersion version, const std::string_view name, - const std::string_view description, sourcemeta::core::JSON input_schema, + const MCPProtocolVersion version, const JSON::StringView name, + const JSON::StringView description, sourcemeta::core::JSON input_schema, std::optional output_schema = std::nullopt, const MCPToolAnnotations &annotations = {}) -> sourcemeta::core::JSON; @@ -447,15 +446,15 @@ auto mcp_make_tool_descriptor( /// handshake. struct MCPImplementation { /// Short machine-readable server name. - std::string_view name; + JSON::StringView name; /// Semver-compatible server version. - std::string_view version; + JSON::StringView version; /// Optional human-readable title. - std::string_view title = {}; + JSON::StringView title = {}; /// Optional human-readable description. - std::string_view description = {}; + JSON::StringView description = {}; /// Optional public website URL. - std::string_view website_url = {}; + JSON::StringView website_url = {}; }; /// @ingroup mcp @@ -500,7 +499,7 @@ SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, const MCPServerCapabilities &capabilities, const MCPImplementation &server, - const std::string_view instructions = {}) + const JSON::StringView instructions = {}) -> sourcemeta::core::JSON; /// @ingroup mcp diff --git a/src/core/mcp/mcp.cc b/src/core/mcp/mcp.cc index 452981d1a..1fc8b5524 100644 --- a/src/core/mcp/mcp.cc +++ b/src/core/mcp/mcp.cc @@ -3,13 +3,12 @@ #include #include -#include // assert -#include // std::size_t -#include // std::optional -#include // std::ostringstream -#include // std::string -#include // std::string_view -#include // std::move +#include // assert +#include // std::size_t +#include // std::optional +#include // std::ostringstream +#include // std::string +#include // std::move namespace { @@ -68,7 +67,7 @@ const auto MCP_HASH_WEBSITE_URL{ namespace sourcemeta::core { -auto mcp_make_text_block(const std::string_view text) +auto mcp_make_text_block(const JSON::StringView text) -> sourcemeta::core::JSON { auto block{sourcemeta::core::JSON::make_object()}; block.assign_assume_new("type", sourcemeta::core::JSON{"text"}, @@ -78,10 +77,10 @@ auto mcp_make_text_block(const std::string_view text) } auto mcp_make_resource_link(const MCPProtocolVersion version, - const std::string_view uri, - const std::string_view mime_type, - const std::string_view name, - const std::string_view description) + const JSON::StringView uri, + const JSON::StringView mime_type, + const JSON::StringView name, + const JSON::StringView description) -> sourcemeta::core::JSON { if (!mcp_supports_resource_link_content(version)) { std::string text; @@ -161,7 +160,7 @@ auto mcp_make_tool_success(const MCPProtocolVersion version, } auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, - const std::string_view message) + const JSON::StringView message) -> sourcemeta::core::JSON { auto content{sourcemeta::core::JSON::make_array()}; content.push_back(mcp_make_text_block(message)); @@ -176,16 +175,16 @@ auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, } auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &identifier, - const std::string_view uri) + const JSON::StringView uri) -> sourcemeta::core::JSON { return sourcemeta::core::jsonrpc_make_error( &identifier, MCP_CODE_RESOURCE_NOT_FOUND, "Resource not found", sourcemeta::core::JSON{uri}); } -auto mcp_make_resource(const std::string_view uri, const std::string_view name, - const std::string_view mime_type, - const std::string_view description, +auto mcp_make_resource(const JSON::StringView uri, const JSON::StringView name, + const JSON::StringView mime_type, + const JSON::StringView description, const std::optional size) -> sourcemeta::core::JSON { auto resource{sourcemeta::core::JSON::make_object()}; @@ -206,9 +205,9 @@ auto mcp_make_resource(const std::string_view uri, const std::string_view name, return resource; } -auto mcp_make_resource_text_content(const std::string_view uri, - const std::string_view mime_type, - const std::string_view text) +auto mcp_make_resource_text_content(const JSON::StringView uri, + const JSON::StringView mime_type, + const JSON::StringView text) -> sourcemeta::core::JSON { auto entry{sourcemeta::core::JSON::make_object()}; entry.assign_assume_new("uri", sourcemeta::core::JSON{uri}, MCP_HASH_URI); @@ -225,10 +224,10 @@ auto mcp_make_resources_read_result(sourcemeta::core::JSON contents) return result; } -auto mcp_make_resource_template(const std::string_view uri_template, - const std::string_view name, - const std::string_view description, - const std::string_view mime_type) +auto mcp_make_resource_template(const JSON::StringView uri_template, + const JSON::StringView name, + const JSON::StringView description, + const JSON::StringView mime_type) -> sourcemeta::core::JSON { auto entry{sourcemeta::core::JSON::make_object()}; entry.assign_assume_new("uriTemplate", sourcemeta::core::JSON{uri_template}, @@ -242,8 +241,8 @@ auto mcp_make_resource_template(const std::string_view uri_template, } auto mcp_make_tool_descriptor( - const MCPProtocolVersion version, const std::string_view name, - const std::string_view description, sourcemeta::core::JSON input_schema, + const MCPProtocolVersion version, const JSON::StringView name, + const JSON::StringView description, sourcemeta::core::JSON input_schema, std::optional output_schema, const MCPToolAnnotations &annotations) -> sourcemeta::core::JSON { assert(!annotations.read_only || !annotations.destructive); @@ -286,7 +285,7 @@ auto mcp_make_tool_descriptor( auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, const MCPServerCapabilities &capabilities, const MCPImplementation &server, - const std::string_view instructions) + const JSON::StringView instructions) -> sourcemeta::core::JSON { const auto *identifier{sourcemeta::core::jsonrpc_request_id(request)}; const auto *parameters{sourcemeta::core::jsonrpc_params(request)}; @@ -295,7 +294,7 @@ auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, return sourcemeta::core::jsonrpc_make_error_invalid_request(identifier); } - std::string_view requested_version{}; + JSON::StringView requested_version{}; const auto *protocol_version_field{ parameters->try_at("protocolVersion", MCP_HASH_PROTOCOL_VERSION)}; if (protocol_version_field != nullptr &&