Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,37 @@ constexpr std::int64_t JSONRPC_CODE_SERVER_ERROR_MAX = -32000;
SOURCEMETA_CORE_JSONRPC_EXPORT
auto jsonrpc_is_server_error(const std::int64_t code) -> bool;

/// @ingroup jsonrpc
/// Check whether the given JSON value is a JSON-RPC 2.0 batch envelope. For
/// example:
///
/// ```cpp
/// #include <sourcemeta/core/json.h>
/// #include <sourcemeta/core/jsonrpc.h>
/// #include <cassert>
///
/// const auto payload{sourcemeta::core::parse_json(R"([])")};
/// assert(sourcemeta::core::jsonrpc_is_batch(payload));
/// ```
SOURCEMETA_CORE_JSONRPC_EXPORT
auto jsonrpc_is_batch(const sourcemeta::core::JSON &payload) -> bool;

/// @ingroup jsonrpc
/// Check whether the given JSON value is a non-empty JSON-RPC 2.0 batch
/// envelope. For example:
///
/// ```cpp
/// #include <sourcemeta/core/json.h>
/// #include <sourcemeta/core/jsonrpc.h>
/// #include <cassert>
///
/// const auto payload{sourcemeta::core::parse_json(
/// R"([ { "jsonrpc": "2.0", "method": "ping" } ])")};
/// assert(sourcemeta::core::jsonrpc_is_valid_batch(payload));
/// ```
SOURCEMETA_CORE_JSONRPC_EXPORT
auto jsonrpc_is_valid_batch(const sourcemeta::core::JSON &payload) -> bool;

/// @ingroup jsonrpc
/// Extract the request identifier from a JSON-RPC 2.0 envelope. Returns a
/// pointer to the identifier (string, number, or null per the specification)
Expand Down
8 changes: 8 additions & 0 deletions src/core/jsonrpc/jsonrpc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ auto jsonrpc_is_server_error(const std::int64_t code) -> bool {
code <= JSONRPC_CODE_SERVER_ERROR_MAX;
}

auto jsonrpc_is_batch(const sourcemeta::core::JSON &payload) -> bool {
return payload.is_array();
}

auto jsonrpc_is_valid_batch(const sourcemeta::core::JSON &payload) -> bool {
return jsonrpc_is_batch(payload) && !payload.empty();
}

auto jsonrpc_request_id(const sourcemeta::core::JSON &request)
-> const sourcemeta::core::JSON * {
if (!request.is_object()) {
Expand Down
8 changes: 8 additions & 0 deletions src/core/mcp/include/sourcemeta/core/mcp.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ constexpr auto mcp_supports_implementation_website_url(
return version == MCPProtocolVersion::V_2025_11_25;
}

/// @ingroup mcp
/// Whether the given protocol version supports JSON-RPC 2.0 batching.
constexpr auto
mcp_supports_jsonrpc_batching(const MCPProtocolVersion version) noexcept
-> bool {
return version == MCPProtocolVersion::V_2025_03_26;
}

/// @ingroup mcp
/// Build an MCP `text` content block carrying the given text payload. For
/// example:
Expand Down
64 changes: 64 additions & 0 deletions test/jsonrpc/jsonrpc_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,70 @@ TEST(JSONRPC, is_server_error_positive_application_code) {
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_server_error(1));
}

TEST(JSONRPC, is_batch_empty_array) {
const auto payload{sourcemeta::core::parse_json(R"([])")};
EXPECT_TRUE(sourcemeta::core::jsonrpc_is_batch(payload));
}

TEST(JSONRPC, is_batch_non_empty_array) {
const auto payload{sourcemeta::core::parse_json(
R"([ { "jsonrpc": "2.0", "method": "ping" } ])")};
EXPECT_TRUE(sourcemeta::core::jsonrpc_is_batch(payload));
}

TEST(JSONRPC, is_batch_object) {
const auto payload{sourcemeta::core::parse_json(
R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_batch(payload));
}

TEST(JSONRPC, is_batch_string) {
const auto payload{sourcemeta::core::parse_json(R"("hello")")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_batch(payload));
}

TEST(JSONRPC, is_batch_null) {
const auto payload{sourcemeta::core::parse_json(R"(null)")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_batch(payload));
}

TEST(JSONRPC, is_batch_integer) {
const auto payload{sourcemeta::core::parse_json(R"(42)")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_batch(payload));
}

TEST(JSONRPC, is_valid_batch_non_empty_array) {
const auto payload{sourcemeta::core::parse_json(
R"([ { "jsonrpc": "2.0", "method": "ping" } ])")};
EXPECT_TRUE(sourcemeta::core::jsonrpc_is_valid_batch(payload));
}

TEST(JSONRPC, is_valid_batch_empty_array) {
const auto payload{sourcemeta::core::parse_json(R"([])")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_valid_batch(payload));
}

TEST(JSONRPC, is_valid_batch_object) {
const auto payload{sourcemeta::core::parse_json(
R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_valid_batch(payload));
}

TEST(JSONRPC, is_valid_batch_string) {
const auto payload{sourcemeta::core::parse_json(R"("hello")")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_valid_batch(payload));
}

TEST(JSONRPC, is_valid_batch_null) {
const auto payload{sourcemeta::core::parse_json(R"(null)")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_valid_batch(payload));
}

TEST(JSONRPC, is_valid_batch_integer) {
const auto payload{sourcemeta::core::parse_json(R"(42)")};
EXPECT_FALSE(sourcemeta::core::jsonrpc_is_valid_batch(payload));
}

TEST(JSONRPC, request_id_integer) {
const auto request{sourcemeta::core::parse_json(
R"({ "jsonrpc": "2.0", "id": 7, "method": "ping" })")};
Expand Down
15 changes: 15 additions & 0 deletions test/mcp/mcp_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,21 @@ TEST(MCP, supports_implementation_website_url_2025_11_25) {
sourcemeta::core::MCPProtocolVersion::V_2025_11_25));
}

TEST(MCP, supports_jsonrpc_batching_2025_03_26) {
EXPECT_TRUE(sourcemeta::core::mcp_supports_jsonrpc_batching(
sourcemeta::core::MCPProtocolVersion::V_2025_03_26));
}

TEST(MCP, supports_jsonrpc_batching_2025_06_18) {
EXPECT_FALSE(sourcemeta::core::mcp_supports_jsonrpc_batching(
sourcemeta::core::MCPProtocolVersion::V_2025_06_18));
}

TEST(MCP, supports_jsonrpc_batching_2025_11_25) {
EXPECT_FALSE(sourcemeta::core::mcp_supports_jsonrpc_batching(
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({
Expand Down
Loading