From a0db67a2e54c9a44997ed23686c06e898aa1267d Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Sat, 23 May 2026 20:30:20 -0400 Subject: [PATCH] Upgrade Core to `178137768156f9c8672dce6ec9d1a95a46ee3eae` Signed-off-by: Juan Cruz Viotti --- CMakeLists.txt | 2 - DEPENDENCIES | 2 +- enterprise/index/CMakeLists.txt | 2 +- enterprise/index/enterprise_index.cc | 4 +- enterprise/server/CMakeLists.txt | 2 +- enterprise/server/action_mcp_v1.cc | 62 +- .../one/enterprise_server_actions.h | 15 +- src/actions/CMakeLists.txt | 2 +- src/actions/action_default_v1.h | 3 +- src/actions/action_dependency_tree_v1.h | 18 +- src/actions/action_health_check_v1.h | 3 +- src/actions/action_jsonschema_evaluate_v1.h | 16 +- src/actions/action_jsonschema_serve_v1.h | 3 +- src/actions/action_jsonschema_trace_v1.h | 16 +- src/actions/action_list_directory_v1.h | 14 +- src/actions/action_mcp_v1.h | 25 +- src/actions/action_not_found_v1.h | 3 +- src/actions/action_schema_search_v1.h | 10 +- .../action_serve_explorer_artifact_v1.h | 16 +- src/actions/action_serve_schema_artifact_v1.h | 16 +- src/actions/action_serve_static_v1.h | 3 +- src/index/CMakeLists.txt | 2 +- src/index/explorer.h | 10 +- src/mcp/CMakeLists.txt | 5 - src/mcp/include/sourcemeta/one/mcp.h | 198 ------- src/mcp/mcp.cc | 419 -------------- src/router/CMakeLists.txt | 2 +- src/router/include/sourcemeta/one/router.h | 9 +- .../actions/actions_schema_directory_test.cc | 2 +- .../actions_uri_to_relative_path_test.cc | 2 +- test/unit/mcp/CMakeLists.txt | 9 - test/unit/mcp/mcp_test.cc | 113 ---- vendor/core/CMakeLists.txt | 9 + vendor/core/config.cmake.in | 9 + vendor/core/src/core/json/construct.h | 4 +- .../include/sourcemeta/core/json_object.h | 12 +- .../jsonrpc/include/sourcemeta/core/jsonrpc.h | 9 +- vendor/core/src/core/jsonrpc/jsonrpc.cc | 63 +-- vendor/core/src/core/mcp/CMakeLists.txt | 9 + .../core/mcp/include/sourcemeta/core/mcp.h | 531 ++++++++++++++++++ vendor/core/src/core/mcp/mcp.cc | 382 +++++++++++++ 41 files changed, 1112 insertions(+), 924 deletions(-) delete mode 100644 src/mcp/CMakeLists.txt delete mode 100644 src/mcp/include/sourcemeta/one/mcp.h delete mode 100644 src/mcp/mcp.cc delete mode 100644 test/unit/mcp/CMakeLists.txt delete mode 100644 test/unit/mcp/mcp_test.cc create mode 100644 vendor/core/src/core/mcp/CMakeLists.txt create mode 100644 vendor/core/src/core/mcp/include/sourcemeta/core/mcp.h create mode 100644 vendor/core/src/core/mcp/mcp.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index e40415e5f..bf42af6ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,6 @@ if(ONE_INDEX OR ONE_SERVER) add_subdirectory(src/metapack) add_subdirectory(src/search) add_subdirectory(src/http) - add_subdirectory(src/mcp) add_subdirectory(src/router) if(ONE_ENTERPRISE) @@ -107,7 +106,6 @@ if(ONE_TESTS) add_subdirectory(test/js) if(ONE_INDEX OR ONE_SERVER) - add_subdirectory(test/unit/mcp) add_subdirectory(test/unit/metapack) add_subdirectory(test/unit/search) endif() diff --git a/DEPENDENCIES b/DEPENDENCIES index 6964ea940..35394275a 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,6 +1,6 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 uwebsockets https://github.com/uNetworking/uWebSockets v20.77.0 -core https://github.com/sourcemeta/core 8018e9d85ef6fc0fd9ccd11c2ae438789214b00a +core https://github.com/sourcemeta/core 178137768156f9c8672dce6ec9d1a95a46ee3eae blaze https://github.com/sourcemeta/blaze bc1f434acafd38803f58a941a756a6f788e556e2 bootstrap https://github.com/twbs/bootstrap v5.3.3 bootstrap-icons https://github.com/twbs/icons v1.11.3 diff --git a/enterprise/index/CMakeLists.txt b/enterprise/index/CMakeLists.txt index 0965cfaae..9080f9149 100644 --- a/enterprise/index/CMakeLists.txt +++ b/enterprise/index/CMakeLists.txt @@ -15,6 +15,6 @@ target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::core::j target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::core::text) target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::one::search) target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::one::actions) -target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::one::mcp) +target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::core::mcp) target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::blaze::foundation) target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::core::uritemplate) diff --git a/enterprise/index/enterprise_index.cc b/enterprise/index/enterprise_index.cc index a6e510654..48de3bb1d 100644 --- a/enterprise/index/enterprise_index.cc +++ b/enterprise/index/enterprise_index.cc @@ -5,12 +5,12 @@ #include #include +#include #include #include #include #include -#include #include #include // assert @@ -87,7 +87,7 @@ auto generate_mcp_resources(const std::filesystem::path &search_metapack_path, std::string uri{configuration.origin}; uri.append(entry.path); - entries.push_back(sourcemeta::one::mcp_make_resource( + entries.push_back(sourcemeta::core::mcp_make_resource( uri, name, "application/schema+json", entry.description, static_cast(entry.bytes_raw))); }); diff --git a/enterprise/server/CMakeLists.txt b/enterprise/server/CMakeLists.txt index 94f2c3c56..f129c78b8 100644 --- a/enterprise/server/CMakeLists.txt +++ b/enterprise/server/CMakeLists.txt @@ -14,6 +14,6 @@ target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::core: target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::core::jsonrpc) target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::core::uri) target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::one::shared) -target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::one::mcp) +target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::core::mcp) target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::one::metapack) target_link_libraries(sourcemeta_one_enterprise_server PRIVATE sourcemeta::blaze::output) diff --git a/enterprise/server/action_mcp_v1.cc b/enterprise/server/action_mcp_v1.cc index 54bdbc288..803a18cd7 100644 --- a/enterprise/server/action_mcp_v1.cc +++ b/enterprise/server/action_mcp_v1.cc @@ -3,10 +3,10 @@ #include #include #include +#include #include #include -#include #include #include // assert @@ -78,17 +78,17 @@ auto ActionMCP_v1::on_resources_list(const sourcemeta::core::JSON &request_json) auto ActionMCP_v1::on_initialize(const sourcemeta::core::JSON &request_json) const -> sourcemeta::core::JSON { const auto &parts{ - this->mcp_metadata_.at(sourcemeta::one::MCP_METHOD_INITIALIZE)}; - return sourcemeta::one::mcp_make_initialize_result( + this->mcp_metadata_.at(sourcemeta::core::MCP_METHOD_INITIALIZE)}; + return sourcemeta::core::mcp_make_initialize_result( request_json, - sourcemeta::one::MCPServerCapabilities{ + sourcemeta::core::MCPServerCapabilities{ .prompts = parts.at(0).to_boolean(), .resources = parts.at(1).to_boolean(), .tools = parts.at(2).to_boolean(), .logging = parts.at(3).to_boolean(), .completions = parts.at(4).to_boolean(), }, - sourcemeta::one::MCPImplementation{ + sourcemeta::core::MCPImplementation{ .name = parts.at(5).to_string(), .version = parts.at(6).to_string(), .title = parts.at(7).to_string(), @@ -99,11 +99,11 @@ auto ActionMCP_v1::on_initialize(const sourcemeta::core::JSON &request_json) } auto ActionMCP_v1::on_tools_list( - const sourcemeta::one::MCPProtocolVersion version, + const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_json) const -> sourcemeta::core::JSON { const auto &precomputed{ - this->mcp_metadata_.at(sourcemeta::one::MCP_METHOD_TOOLS_LIST)}; + this->mcp_metadata_.at(sourcemeta::core::MCP_METHOD_TOOLS_LIST)}; auto tools{sourcemeta::core::JSON::make_array()}; for (const auto &tool : precomputed.as_array()) { @@ -111,10 +111,10 @@ auto ActionMCP_v1::on_tools_list( if (!tool.at(3).is_null()) { output_schema = tool.at(3); } - tools.push_back(sourcemeta::one::mcp_make_tool_descriptor( + tools.push_back(sourcemeta::core::mcp_make_tool_descriptor( version, tool.at(0).to_string(), tool.at(1).to_string(), tool.at(2), std::move(output_schema), - sourcemeta::one::MCPToolAnnotations{ + sourcemeta::core::MCPToolAnnotations{ .title = tool.at(4).to_string(), .read_only = tool.at(5).to_boolean(), .destructive = tool.at(6).to_boolean(), @@ -138,7 +138,7 @@ auto ActionMCP_v1::on_resources_read(const sourcemeta::core::JSON &request_json) sourcemeta::core::URI request{uri}; request.relative_to(sourcemeta::core::URI{this->server_uri()}); if (request.is_absolute()) { - return sourcemeta::one::mcp_make_error_resource_not_found(id, uri); + return sourcemeta::core::mcp_make_error_resource_not_found(id, uri); } const auto path{request.path().value_or("")}; std::string_view schema_path{path}; @@ -146,7 +146,7 @@ auto ActionMCP_v1::on_resources_read(const sourcemeta::core::JSON &request_json) schema_path.remove_suffix(5); } if (schema_path.empty()) { - return sourcemeta::one::mcp_make_error_resource_not_found(id, uri); + return sourcemeta::core::mcp_make_error_resource_not_found(id, uri); } const auto query{request.query()}; const auto bundle{query.has_value() && @@ -154,30 +154,31 @@ auto ActionMCP_v1::on_resources_read(const sourcemeta::core::JSON &request_json) resolved = sourcemeta::core::weakly_canonical( this->resolve_schema_path(schema_path, bundle)); } catch (const std::exception &) { - return sourcemeta::one::mcp_make_error_resource_not_found(id, uri); + return sourcemeta::core::mcp_make_error_resource_not_found(id, uri); } if (!sourcemeta::core::is_under_path(resolved, this->base() / "schemas")) { - return sourcemeta::one::mcp_make_error_resource_not_found(id, uri); + return sourcemeta::core::mcp_make_error_resource_not_found(id, uri); } const auto schema{sourcemeta::one::metapack_read_json(resolved)}; if (!schema.has_value()) { - return sourcemeta::one::mcp_make_error_resource_not_found(id, uri); + return sourcemeta::core::mcp_make_error_resource_not_found(id, uri); } std::ostringstream payload; sourcemeta::core::prettify(schema.value(), payload); auto contents{sourcemeta::core::JSON::make_array()}; - contents.push_back(sourcemeta::one::mcp_make_resource_text_content( + contents.push_back(sourcemeta::core::mcp_make_resource_text_content( uri, MCP_TEMPLATE_MIME_TYPE, payload.str())); return sourcemeta::core::jsonrpc_make_success( - id, sourcemeta::one::mcp_make_resources_read_result(std::move(contents))); + id, + sourcemeta::core::mcp_make_resources_read_result(std::move(contents))); } auto ActionMCP_v1::on_tools_call( - const sourcemeta::one::MCPProtocolVersion version, + const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_json, const std::string_view envelope) -> sourcemeta::core::JSON { const auto &id{request_json.at("id")}; @@ -194,21 +195,22 @@ auto ActionMCP_v1::on_tools_call( if (instance == nullptr) [[unlikely]] { return sourcemeta::core::jsonrpc_make_error_internal(&id); } - const auto *arguments{sourcemeta::one::mcp_tool_call_arguments(request_json)}; + const auto *arguments{ + sourcemeta::core::mcp_tool_call_arguments(request_json)}; if (arguments == nullptr) { return sourcemeta::core::jsonrpc_make_error_invalid_params(id); } try { return instance->mcp(version, id, *arguments, envelope); } catch (const std::exception &error) { - return sourcemeta::one::mcp_make_tool_error(id, error.what()); + return sourcemeta::core::mcp_make_tool_error(id, error.what()); } } -auto ActionMCP_v1::on_message(const sourcemeta::one::MCPProtocolVersion version, - sourcemeta::one::HTTPRequest &request, - sourcemeta::one::HTTPResponse &response, - std::string &&body) -> void { +auto ActionMCP_v1::on_message( + const sourcemeta::core::MCPProtocolVersion version, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response, std::string &&body) -> void { sourcemeta::core::JSON request_json{nullptr}; try { request_json = sourcemeta::core::parse_json(body); @@ -219,7 +221,7 @@ auto ActionMCP_v1::on_message(const sourcemeta::one::MCPProtocolVersion version, } if (request_json.is_array()) { - if (version == sourcemeta::one::MCPProtocolVersion::V_2025_03_26) { + if (version == sourcemeta::core::MCPProtocolVersion::V_2025_03_26) { // TODO: Support batches for strict compliance to MCP 2025-03-26 this->write_envelope( request, response, sourcemeta::one::STATUS_OK, @@ -256,19 +258,19 @@ auto ActionMCP_v1::on_message(const sourcemeta::one::MCPProtocolVersion version, assert(id != nullptr); const auto method{sourcemeta::core::jsonrpc_method(request_json)}; sourcemeta::core::JSON envelope{nullptr}; - if (!sourcemeta::one::mcp_is_request_method(method)) { + if (!sourcemeta::core::mcp_is_request_method(method)) { envelope = sourcemeta::core::jsonrpc_make_error_method_not_found(*id); } else if (!this->validate(this->request_schema_, request_json)) { envelope = sourcemeta::core::jsonrpc_make_error_invalid_request(id); - } else if (method == sourcemeta::one::MCP_METHOD_INITIALIZE) { + } else if (method == sourcemeta::core::MCP_METHOD_INITIALIZE) { envelope = this->on_initialize(request_json); - } else if (method == sourcemeta::one::MCP_METHOD_TOOLS_LIST) { + } else if (method == sourcemeta::core::MCP_METHOD_TOOLS_LIST) { envelope = this->on_tools_list(version, request_json); - } else if (method == sourcemeta::one::MCP_METHOD_RESOURCES_LIST) { + } else if (method == sourcemeta::core::MCP_METHOD_RESOURCES_LIST) { envelope = this->on_resources_list(request_json); - } else if (method == sourcemeta::one::MCP_METHOD_RESOURCES_READ) { + } else if (method == sourcemeta::core::MCP_METHOD_RESOURCES_READ) { envelope = this->on_resources_read(request_json); - } else if (method == sourcemeta::one::MCP_METHOD_TOOLS_CALL) { + } else if (method == sourcemeta::core::MCP_METHOD_TOOLS_CALL) { envelope = this->on_tools_call(version, request_json, body); } else if (this->mcp_metadata_.defines(method)) { envelope = sourcemeta::core::jsonrpc_make_success( diff --git a/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h b/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h index d763b5b20..1bd270a65 100644 --- a/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h +++ b/enterprise/server/include/sourcemeta/one/enterprise_server_actions.h @@ -3,10 +3,10 @@ #include #include +#include #include #include -#include #include #include @@ -86,8 +86,9 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { return; } - const auto negotiated_version{sourcemeta::one::mcp_resolve_protocol_version( - request.header("mcp-protocol-version"))}; + const auto negotiated_version{ + sourcemeta::core::mcp_resolve_protocol_version( + request.header("mcp-protocol-version"))}; if (!negotiated_version.has_value()) { this->write_envelope(request, response, sourcemeta::one::STATUS_BAD_REQUEST, @@ -125,7 +126,7 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { }); } - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); @@ -154,18 +155,18 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { auto on_initialize(const sourcemeta::core::JSON &request_json) const -> sourcemeta::core::JSON; - auto on_tools_list(sourcemeta::one::MCPProtocolVersion version, + auto on_tools_list(sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_json) const -> sourcemeta::core::JSON; auto on_resources_read(const sourcemeta::core::JSON &request_json) const -> sourcemeta::core::JSON; - auto on_tools_call(sourcemeta::one::MCPProtocolVersion version, + auto on_tools_call(sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_json, std::string_view envelope) -> sourcemeta::core::JSON; - auto on_message(sourcemeta::one::MCPProtocolVersion version, + auto on_message(sourcemeta::core::MCPProtocolVersion version, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response, std::string &&body) -> void; diff --git a/src/actions/CMakeLists.txt b/src/actions/CMakeLists.txt index 7a1d901db..c02481b81 100644 --- a/src/actions/CMakeLists.txt +++ b/src/actions/CMakeLists.txt @@ -21,7 +21,7 @@ target_link_libraries(sourcemeta_one_actions PUBLIC sourcemeta::core::uritemplat target_link_libraries(sourcemeta_one_actions PRIVATE sourcemeta::core::json) target_link_libraries(sourcemeta_one_actions PRIVATE sourcemeta::core::uri) target_link_libraries(sourcemeta_one_actions PUBLIC sourcemeta::core::jsonrpc) -target_link_libraries(sourcemeta_one_actions PRIVATE sourcemeta::one::mcp) +target_link_libraries(sourcemeta_one_actions PRIVATE sourcemeta::core::mcp) target_link_libraries(sourcemeta_one_actions PRIVATE sourcemeta::core::jsonpointer) target_link_libraries(sourcemeta_one_actions PRIVATE sourcemeta::core::io) target_link_libraries(sourcemeta_one_actions PRIVATE sourcemeta::core::time) diff --git a/src/actions/action_default_v1.h b/src/actions/action_default_v1.h index 2b31d938e..407263d04 100644 --- a/src/actions/action_default_v1.h +++ b/src/actions/action_default_v1.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -114,7 +115,7 @@ class ActionDefault_v1 : public sourcemeta::one::RouterAction { } } - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); diff --git a/src/actions/action_dependency_tree_v1.h b/src/actions/action_dependency_tree_v1.h index 6fa2ca6d0..f80bf461d 100644 --- a/src/actions/action_dependency_tree_v1.h +++ b/src/actions/action_dependency_tree_v1.h @@ -4,11 +4,11 @@ #include #include #include +#include #include #include #include -#include #include #include @@ -81,7 +81,7 @@ class ActionDependencyTree_v1 : public sourcemeta::one::RouterAction { response, this->error_schema_); } - auto mcp(const sourcemeta::one::MCPProtocolVersion version, + auto mcp(const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_id, const sourcemeta::core::JSON &arguments, const std::string_view) -> sourcemeta::core::JSON override { @@ -96,15 +96,15 @@ class ActionDependencyTree_v1 : public sourcemeta::one::RouterAction { const auto directory{ this->schema_directory(arguments.at("schema").to_string())}; if (!directory.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } auto contents{sourcemeta::one::metapack_read_json(directory.value() / this->metapack_)}; if (!contents.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } auto &result{contents.value()}; @@ -124,13 +124,13 @@ class ActionDependencyTree_v1 : public sourcemeta::one::RouterAction { auto content{sourcemeta::core::JSON::make_array()}; std::ostringstream payload; sourcemeta::core::prettify(result, payload); - content.push_back(sourcemeta::one::mcp_make_text_block(payload.str())); + content.push_back(sourcemeta::core::mcp_make_text_block(payload.str())); for (const auto uri : unique_uris) { - content.push_back(sourcemeta::one::mcp_make_resource_link( + content.push_back(sourcemeta::core::mcp_make_resource_link( version, uri, "application/schema+json")); } - return sourcemeta::one::mcp_make_tool_success( + return sourcemeta::core::mcp_make_tool_success( version, request_id, std::move(result), std::move(content)); } diff --git a/src/actions/action_health_check_v1.h b/src/actions/action_health_check_v1.h index 757fa718e..c07e313ae 100644 --- a/src/actions/action_health_check_v1.h +++ b/src/actions/action_health_check_v1.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -53,7 +54,7 @@ class ActionHealthCheck_v1 : public sourcemeta::one::RouterAction { response); } - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); diff --git a/src/actions/action_jsonschema_evaluate_v1.h b/src/actions/action_jsonschema_evaluate_v1.h index 7330b77f0..f5e028d92 100644 --- a/src/actions/action_jsonschema_evaluate_v1.h +++ b/src/actions/action_jsonschema_evaluate_v1.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -10,7 +11,6 @@ #include #include -#include #include #include #include @@ -68,7 +68,7 @@ class ActionJSONSchemaEvaluate_v1 : public sourcemeta::one::RouterAction { }); } - auto mcp(const sourcemeta::one::MCPProtocolVersion version, + auto mcp(const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_id, const sourcemeta::core::JSON &arguments, const std::string_view) -> sourcemeta::core::JSON override { @@ -83,23 +83,23 @@ class ActionJSONSchemaEvaluate_v1 : public sourcemeta::one::RouterAction { const auto directory{ this->schema_directory(arguments.at("schema").to_string())}; if (!directory.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } const auto template_path{directory.value() / "blaze-exhaustive.metapack"}; if (!std::filesystem::exists(template_path)) { const auto schema_path{directory.value() / "schema.metapack"}; if (std::filesystem::exists(schema_path)) { - return sourcemeta::one::mcp_make_tool_error( + return sourcemeta::core::mcp_make_tool_error( request_id, "This schema was not precompiled for schema evaluation"); } - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } - return sourcemeta::one::mcp_make_tool_success( + return sourcemeta::core::mcp_make_tool_success( version, request_id, this->evaluate(template_path, arguments.at("instance"))); } diff --git a/src/actions/action_jsonschema_serve_v1.h b/src/actions/action_jsonschema_serve_v1.h index 518f7cd99..a7f891ec3 100644 --- a/src/actions/action_jsonschema_serve_v1.h +++ b/src/actions/action_jsonschema_serve_v1.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -85,7 +86,7 @@ class ActionJSONSchemaServe_v1 : public sourcemeta::one::RouterAction { serve(*this, matches.front(), request, response, this->error_schema_); } - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); diff --git a/src/actions/action_jsonschema_trace_v1.h b/src/actions/action_jsonschema_trace_v1.h index 6b0bbb8ec..d2cb9c1d1 100644 --- a/src/actions/action_jsonschema_trace_v1.h +++ b/src/actions/action_jsonschema_trace_v1.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -12,7 +13,6 @@ #include #include -#include #include #include #include @@ -70,7 +70,7 @@ class ActionJSONSchemaTrace_v1 : public sourcemeta::one::RouterAction { }); } - auto mcp(const sourcemeta::one::MCPProtocolVersion version, + auto mcp(const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_id, const sourcemeta::core::JSON &arguments, const std::string_view envelope) -> sourcemeta::core::JSON override { @@ -85,20 +85,20 @@ class ActionJSONSchemaTrace_v1 : public sourcemeta::one::RouterAction { const auto directory{ this->schema_directory(arguments.at("schema").to_string())}; if (!directory.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } const auto template_path{directory.value() / "blaze-exhaustive.metapack"}; if (!std::filesystem::exists(template_path)) { const auto schema_path{directory.value() / "schema.metapack"}; if (std::filesystem::exists(schema_path)) { - return sourcemeta::one::mcp_make_tool_error( + return sourcemeta::core::mcp_make_tool_error( request_id, "This schema was not precompiled for schema evaluation"); } - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } sourcemeta::core::PointerPositionTracker tracker; @@ -107,7 +107,7 @@ class ActionJSONSchemaTrace_v1 : public sourcemeta::one::RouterAction { const auto schema_template{ ActionJSONSchemaTrace_v1::compile_template(template_path)}; sourcemeta::blaze::Evaluator evaluator; - return sourcemeta::one::mcp_make_tool_success( + return sourcemeta::core::mcp_make_tool_success( version, request_id, this->trace( evaluator, schema_template, arguments.at("instance"), template_path, diff --git a/src/actions/action_list_directory_v1.h b/src/actions/action_list_directory_v1.h index a0fe6c223..37cde837c 100644 --- a/src/actions/action_list_directory_v1.h +++ b/src/actions/action_list_directory_v1.h @@ -4,10 +4,10 @@ #include #include #include +#include #include #include -#include #include #include @@ -71,7 +71,7 @@ class ActionListDirectory_v1 : public sourcemeta::one::RouterAction { response, this->error_schema_); } - auto mcp(const sourcemeta::one::MCPProtocolVersion version, + auto mcp(const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_id, const sourcemeta::core::JSON &arguments, const std::string_view) -> sourcemeta::core::JSON override { @@ -97,8 +97,8 @@ class ActionListDirectory_v1 : public sourcemeta::one::RouterAction { auto contents{sourcemeta::one::metapack_read_json(safe_path)}; if (!contents.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Directory not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Directory not found"); } auto &result{contents.value()}; @@ -106,7 +106,7 @@ class ActionListDirectory_v1 : public sourcemeta::one::RouterAction { std::ostringstream payload; sourcemeta::core::prettify(result, payload); - content.push_back(sourcemeta::one::mcp_make_text_block(payload.str())); + content.push_back(sourcemeta::core::mcp_make_text_block(payload.str())); if (const auto *entries{result.try_at("entries")}; entries != nullptr && entries->is_array()) { @@ -117,7 +117,7 @@ class ActionListDirectory_v1 : public sourcemeta::one::RouterAction { if (!entry.defines("identifier")) { continue; } - content.push_back(sourcemeta::one::mcp_make_resource_link( + content.push_back(sourcemeta::core::mcp_make_resource_link( version, entry.at("identifier").to_string(), "application/schema+json", entry.at_or("title", EMPTY_STRING).to_string(), @@ -125,7 +125,7 @@ class ActionListDirectory_v1 : public sourcemeta::one::RouterAction { } } - return sourcemeta::one::mcp_make_tool_success( + return sourcemeta::core::mcp_make_tool_success( version, request_id, std::move(result), std::move(content)); } diff --git a/src/actions/action_mcp_v1.h b/src/actions/action_mcp_v1.h index 43a43f631..4adab86ab 100644 --- a/src/actions/action_mcp_v1.h +++ b/src/actions/action_mcp_v1.h @@ -11,11 +11,11 @@ using ActionMCP_v1 = sourcemeta::one::enterprise::ActionMCP_v1; #include #include +#include #include #include #include -#include #include #include #include @@ -85,8 +85,9 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { return; } - const auto negotiated_version{sourcemeta::one::mcp_resolve_protocol_version( - request.header("mcp-protocol-version"))}; + const auto negotiated_version{ + sourcemeta::core::mcp_resolve_protocol_version( + request.header("mcp-protocol-version"))}; if (!negotiated_version.has_value()) { this->write_envelope(request, response, sourcemeta::one::STATUS_BAD_REQUEST, @@ -120,7 +121,7 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { // TODO: Support batches for strict compliance to MCP 2025-03-26 this->write_envelope( callback_request, callback_response, sourcemeta::one::STATUS_OK, - version == sourcemeta::one::MCPProtocolVersion::V_2025_03_26 + version == sourcemeta::core::MCPProtocolVersion::V_2025_03_26 ? sourcemeta::core::jsonrpc_make_error( nullptr, 6, "Unsupported operation", sourcemeta::core::JSON{ @@ -148,7 +149,7 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { }); } - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); @@ -195,19 +196,19 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { return this->enterprise_required(nullptr); } - if (method == sourcemeta::one::MCP_METHOD_INITIALIZE) { + if (method == sourcemeta::core::MCP_METHOD_INITIALIZE) { const auto &parts{ - this->mcp_metadata_.at(sourcemeta::one::MCP_METHOD_INITIALIZE)}; - return sourcemeta::one::mcp_make_initialize_result( + this->mcp_metadata_.at(sourcemeta::core::MCP_METHOD_INITIALIZE)}; + return sourcemeta::core::mcp_make_initialize_result( request_json, - sourcemeta::one::MCPServerCapabilities{ + sourcemeta::core::MCPServerCapabilities{ .prompts = parts.at(0).to_boolean(), .resources = parts.at(1).to_boolean(), .tools = parts.at(2).to_boolean(), .logging = parts.at(3).to_boolean(), .completions = parts.at(4).to_boolean(), }, - sourcemeta::one::MCPImplementation{ + sourcemeta::core::MCPImplementation{ .name = parts.at(5).to_string(), .version = parts.at(6).to_string(), .title = parts.at(7).to_string(), @@ -217,10 +218,10 @@ class ActionMCP_v1 : public sourcemeta::one::RouterAction { parts.at(10).to_string()); } - if (method == sourcemeta::one::MCP_METHOD_RESOURCES_TEMPLATES_LIST) { + if (method == sourcemeta::core::MCP_METHOD_RESOURCES_TEMPLATES_LIST) { return sourcemeta::core::jsonrpc_make_success( *id, this->mcp_metadata_.at( - sourcemeta::one::MCP_METHOD_RESOURCES_TEMPLATES_LIST)); + sourcemeta::core::MCP_METHOD_RESOURCES_TEMPLATES_LIST)); } return this->enterprise_required(id); diff --git a/src/actions/action_not_found_v1.h b/src/actions/action_not_found_v1.h index 5200a2f33..212b64756 100644 --- a/src/actions/action_not_found_v1.h +++ b/src/actions/action_not_found_v1.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -44,7 +45,7 @@ class ActionNotFound_v1 : public sourcemeta::one::RouterAction { "There is nothing at this URL", this->error_schema_); } - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); diff --git a/src/actions/action_schema_search_v1.h b/src/actions/action_schema_search_v1.h index 27ebee00e..89609b1a0 100644 --- a/src/actions/action_schema_search_v1.h +++ b/src/actions/action_schema_search_v1.h @@ -4,10 +4,10 @@ #include #include #include +#include #include #include -#include #include #include @@ -164,7 +164,7 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::RouterAction { sourcemeta::one::Encoding::Identity); } - auto mcp(const sourcemeta::one::MCPProtocolVersion version, + auto mcp(const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_id, const sourcemeta::core::JSON &arguments, const std::string_view) -> sourcemeta::core::JSON override { @@ -202,17 +202,17 @@ class ActionSchemaSearch_v1 : public sourcemeta::one::RouterAction { std::ostringstream payload; sourcemeta::core::prettify(result, payload); - content.push_back(sourcemeta::one::mcp_make_text_block(payload.str())); + content.push_back(sourcemeta::core::mcp_make_text_block(payload.str())); for (std::size_t index{0}; index < result.array_size(); ++index) { const auto &entry{result.at(index)}; - content.push_back(sourcemeta::one::mcp_make_resource_link( + content.push_back(sourcemeta::core::mcp_make_resource_link( version, entry.at("identifier").to_string(), "application/schema+json", entry.at("title").to_string(), entry.at("description").to_string())); } - return sourcemeta::one::mcp_make_tool_success( + return sourcemeta::core::mcp_make_tool_success( version, request_id, std::move(result), std::move(content)); } diff --git a/src/actions/action_serve_explorer_artifact_v1.h b/src/actions/action_serve_explorer_artifact_v1.h index 45660549f..d09e8e75c 100644 --- a/src/actions/action_serve_explorer_artifact_v1.h +++ b/src/actions/action_serve_explorer_artifact_v1.h @@ -3,11 +3,11 @@ #include #include +#include #include #include #include -#include #include #include @@ -62,7 +62,7 @@ class ActionServeExplorerArtifact_v1 : public sourcemeta::one::RouterAction { response, this->error_schema_); } - auto mcp(const sourcemeta::one::MCPProtocolVersion version, + auto mcp(const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_id, const sourcemeta::core::JSON &arguments, const std::string_view) -> sourcemeta::core::JSON override { @@ -77,8 +77,8 @@ class ActionServeExplorerArtifact_v1 : public sourcemeta::one::RouterAction { const auto schema_path{ this->uri_to_relative_path(arguments.at("schema").to_string())}; if (!schema_path.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } auto absolute_path{this->base() / "explorer" / schema_path.value() / "%"}; @@ -86,12 +86,12 @@ class ActionServeExplorerArtifact_v1 : public sourcemeta::one::RouterAction { auto contents{sourcemeta::one::metapack_read_json(absolute_path)}; if (!contents.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } - return sourcemeta::one::mcp_make_tool_success(version, request_id, - std::move(contents).value()); + return sourcemeta::core::mcp_make_tool_success(version, request_id, + std::move(contents).value()); } private: diff --git a/src/actions/action_serve_schema_artifact_v1.h b/src/actions/action_serve_schema_artifact_v1.h index 4ada3d4cd..1ad808f2b 100644 --- a/src/actions/action_serve_schema_artifact_v1.h +++ b/src/actions/action_serve_schema_artifact_v1.h @@ -3,11 +3,11 @@ #include #include +#include #include #include #include -#include #include #include @@ -67,7 +67,7 @@ class ActionServeSchemaArtifact_v1 : public sourcemeta::one::RouterAction { response, this->error_schema_); } - auto mcp(const sourcemeta::one::MCPProtocolVersion version, + auto mcp(const sourcemeta::core::MCPProtocolVersion version, const sourcemeta::core::JSON &request_id, const sourcemeta::core::JSON &arguments, const std::string_view) -> sourcemeta::core::JSON override { @@ -82,19 +82,19 @@ class ActionServeSchemaArtifact_v1 : public sourcemeta::one::RouterAction { const auto directory{ this->schema_directory(arguments.at("schema").to_string())}; if (!directory.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } auto contents{sourcemeta::one::metapack_read_json( directory.value() / (std::string{this->artifact_} + ".metapack"))}; if (!contents.has_value()) { - return sourcemeta::one::mcp_make_tool_error(request_id, - "Schema not found"); + return sourcemeta::core::mcp_make_tool_error(request_id, + "Schema not found"); } - return sourcemeta::one::mcp_make_tool_success(version, request_id, - std::move(contents).value()); + return sourcemeta::core::mcp_make_tool_success(version, request_id, + std::move(contents).value()); } private: diff --git a/src/actions/action_serve_static_v1.h b/src/actions/action_serve_static_v1.h index fb112e923..733437247 100644 --- a/src/actions/action_serve_static_v1.h +++ b/src/actions/action_serve_static_v1.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -63,7 +64,7 @@ class ActionServeStatic_v1 : public sourcemeta::one::RouterAction { request, response, this->error_schema_); } - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); diff --git a/src/index/CMakeLists.txt b/src/index/CMakeLists.txt index 34690765b..f20490629 100644 --- a/src/index/CMakeLists.txt +++ b/src/index/CMakeLists.txt @@ -16,7 +16,7 @@ endif() target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::one::actions) target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::one::resolver) target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::one::shared) -target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::one::mcp) +target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::core::mcp) target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::one::metapack) target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::one::search) target_link_libraries(sourcemeta_one_index PRIVATE sourcemeta::one::configuration) diff --git a/src/index/explorer.h b/src/index/explorer.h index 6d4c174c9..832ce59e1 100644 --- a/src/index/explorer.h +++ b/src/index/explorer.h @@ -2,7 +2,6 @@ #define SOURCEMETA_ONE_INDEX_EXPLORER_H_ #include -#include #include #include #include @@ -10,6 +9,7 @@ #include #include +#include #include #include @@ -592,7 +592,7 @@ struct GENERATE_MCP { template_uri.append("/{+path}{?bundle}"); auto resource_templates{sourcemeta::core::JSON::make_array()}; - resource_templates.push_back(sourcemeta::one::mcp_make_resource_template( + resource_templates.push_back(sourcemeta::core::mcp_make_resource_template( template_uri, "JSON Schema", "A JSON Schema in this catalog (optionally bundled)", "application/schema+json")); @@ -637,13 +637,13 @@ struct GENERATE_MCP { auto document{sourcemeta::core::JSON::make_object()}; document.assign("origin", sourcemeta::core::JSON{configuration.origin}); - document.assign(std::string{sourcemeta::one::MCP_METHOD_INITIALIZE}, + document.assign(std::string{sourcemeta::core::MCP_METHOD_INITIALIZE}, std::move(initialize_ingredients)); document.assign( - std::string{sourcemeta::one::MCP_METHOD_RESOURCES_TEMPLATES_LIST}, + std::string{sourcemeta::core::MCP_METHOD_RESOURCES_TEMPLATES_LIST}, std::move(resource_templates_response)); document.assign("resources", std::move(resources)); - document.assign(std::string{sourcemeta::one::MCP_METHOD_TOOLS_LIST}, + document.assign(std::string{sourcemeta::core::MCP_METHOD_TOOLS_LIST}, std::move(tools)); document.assign("toolRoutes", std::move(tool_routes)); diff --git a/src/mcp/CMakeLists.txt b/src/mcp/CMakeLists.txt deleted file mode 100644 index 61adf1e1f..000000000 --- a/src/mcp/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME mcp - SOURCES mcp.cc) - -target_link_libraries(sourcemeta_one_mcp PUBLIC sourcemeta::core::json) -target_link_libraries(sourcemeta_one_mcp PUBLIC sourcemeta::core::jsonrpc) diff --git a/src/mcp/include/sourcemeta/one/mcp.h b/src/mcp/include/sourcemeta/one/mcp.h deleted file mode 100644 index cf5e372e8..000000000 --- a/src/mcp/include/sourcemeta/one/mcp.h +++ /dev/null @@ -1,198 +0,0 @@ -#ifndef SOURCEMETA_ONE_MCP_H -#define SOURCEMETA_ONE_MCP_H - -#include -#include - -#include // std::size_t -#include // std::int64_t, std::uint8_t -#include // std::optional -#include // std::string_view -#include // std::unreachable - -namespace sourcemeta::one { - -enum class MCPProtocolVersion : std::uint8_t { - V_2025_03_26, - V_2025_06_18, - V_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(); -} - -constexpr std::string_view MCP_METHOD_INITIALIZE{"initialize"}; -constexpr std::string_view MCP_METHOD_PING{"ping"}; -constexpr std::string_view MCP_METHOD_TOOLS_LIST{"tools/list"}; -constexpr std::string_view MCP_METHOD_TOOLS_CALL{"tools/call"}; -constexpr std::string_view MCP_METHOD_RESOURCES_LIST{"resources/list"}; -constexpr std::string_view MCP_METHOD_RESOURCES_READ{"resources/read"}; -constexpr std::string_view MCP_METHOD_RESOURCES_TEMPLATES_LIST{ - "resources/templates/list"}; -constexpr std::string_view MCP_METHOD_NOTIFICATIONS_INITIALIZED{ - "notifications/initialized"}; - -constexpr std::int64_t MCP_CODE_RESOURCE_NOT_FOUND{-32002}; -constexpr std::int64_t MCP_CODE_URL_ELICITATION_REQUIRED{-32042}; - -auto mcp_make_text_block(const std::string_view text) -> sourcemeta::core::JSON; - -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; - -auto mcp_make_tool_success(const MCPProtocolVersion version, - const sourcemeta::core::JSON &id, - sourcemeta::core::JSON result) - -> sourcemeta::core::JSON; - -auto mcp_make_tool_success(const MCPProtocolVersion version, - const sourcemeta::core::JSON &id, - sourcemeta::core::JSON structured, - sourcemeta::core::JSON content_blocks) - -> sourcemeta::core::JSON; - -auto mcp_make_tool_error(const sourcemeta::core::JSON &id, - const std::string_view message) - -> sourcemeta::core::JSON; - -auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &id, - const std::string_view uri) - -> sourcemeta::core::JSON; - -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; - -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 mcp_make_resources_read_result(sourcemeta::core::JSON contents) - -> sourcemeta::core::JSON; - -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; - -struct MCPToolAnnotations { - std::string_view title = {}; - bool read_only = false; - bool destructive = true; - bool idempotent = false; - bool open_world = true; -}; - -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; - -struct MCPImplementation { - std::string_view name; - std::string_view version; - std::string_view title = {}; - std::string_view description = {}; - std::string_view website_url = {}; -}; - -struct MCPServerCapabilities { - bool prompts = false; - bool resources = false; - bool tools = false; - bool logging = false; - bool completions = false; -}; - -auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, - const MCPServerCapabilities &capabilities, - const MCPImplementation &server, - const std::string_view instructions = {}) - -> sourcemeta::core::JSON; - -auto mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) - -> const sourcemeta::core::JSON *; - -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; -} - -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; -} - -constexpr auto -mcp_supports_output_schema(const MCPProtocolVersion version) noexcept -> bool { - return version != MCPProtocolVersion::V_2025_03_26; -} - -constexpr auto -mcp_supports_structured_content(const MCPProtocolVersion version) noexcept - -> bool { - return version != MCPProtocolVersion::V_2025_03_26; -} - -constexpr auto -mcp_supports_resource_link_content(const MCPProtocolVersion version) noexcept - -> bool { - return version != MCPProtocolVersion::V_2025_03_26; -} - -constexpr auto -mcp_supports_implementation_title(const MCPProtocolVersion version) noexcept - -> bool { - return version != MCPProtocolVersion::V_2025_03_26; -} - -constexpr auto mcp_supports_implementation_description( - const MCPProtocolVersion version) noexcept -> bool { - return version == MCPProtocolVersion::V_2025_11_25; -} - -constexpr auto mcp_supports_implementation_website_url( - const MCPProtocolVersion version) noexcept -> bool { - return version == MCPProtocolVersion::V_2025_11_25; -} - -} // namespace sourcemeta::one - -#endif diff --git a/src/mcp/mcp.cc b/src/mcp/mcp.cc deleted file mode 100644 index e3b0db792..000000000 --- a/src/mcp/mcp.cc +++ /dev/null @@ -1,419 +0,0 @@ -#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::make_object().as_object().hash("annotations")}; -const auto MCP_HASH_ARGUMENTS{ - sourcemeta::core::JSON::make_object().as_object().hash("arguments")}; -const auto MCP_HASH_CAPABILITIES{ - sourcemeta::core::JSON::make_object().as_object().hash("capabilities")}; -const auto MCP_HASH_COMPLETIONS{ - sourcemeta::core::JSON::make_object().as_object().hash("completions")}; -const auto MCP_HASH_CONTENT{ - sourcemeta::core::JSON::make_object().as_object().hash("content")}; -const auto MCP_HASH_CONTENTS{ - sourcemeta::core::JSON::make_object().as_object().hash("contents")}; -const auto MCP_HASH_DESCRIPTION{ - sourcemeta::core::JSON::make_object().as_object().hash("description")}; -const auto MCP_HASH_DESTRUCTIVE_HINT{ - sourcemeta::core::JSON::make_object().as_object().hash("destructiveHint")}; -const auto MCP_HASH_IDEMPOTENT_HINT{ - sourcemeta::core::JSON::make_object().as_object().hash("idempotentHint")}; -const auto MCP_HASH_INPUT_SCHEMA{ - sourcemeta::core::JSON::make_object().as_object().hash("inputSchema")}; -const auto MCP_HASH_INSTRUCTIONS{ - sourcemeta::core::JSON::make_object().as_object().hash("instructions")}; -const auto MCP_HASH_IS_ERROR{ - sourcemeta::core::JSON::make_object().as_object().hash("isError")}; -const auto MCP_HASH_LOGGING{ - sourcemeta::core::JSON::make_object().as_object().hash("logging")}; -const auto MCP_HASH_MIME_TYPE{ - sourcemeta::core::JSON::make_object().as_object().hash("mimeType")}; -const auto MCP_HASH_NAME{ - sourcemeta::core::JSON::make_object().as_object().hash("name")}; -const auto MCP_HASH_OPEN_WORLD_HINT{ - sourcemeta::core::JSON::make_object().as_object().hash("openWorldHint")}; -const auto MCP_HASH_OUTPUT_SCHEMA{ - sourcemeta::core::JSON::make_object().as_object().hash("outputSchema")}; -const auto MCP_HASH_PROMPTS{ - sourcemeta::core::JSON::make_object().as_object().hash("prompts")}; -const auto MCP_HASH_PROTOCOL_VERSION{ - sourcemeta::core::JSON::make_object().as_object().hash("protocolVersion")}; -const auto MCP_HASH_READ_ONLY_HINT{ - sourcemeta::core::JSON::make_object().as_object().hash("readOnlyHint")}; -const auto MCP_HASH_RESOURCES{ - sourcemeta::core::JSON::make_object().as_object().hash("resources")}; -const auto MCP_HASH_SERVER_INFO{ - sourcemeta::core::JSON::make_object().as_object().hash("serverInfo")}; -const auto MCP_HASH_SIZE{ - sourcemeta::core::JSON::make_object().as_object().hash("size")}; -const auto MCP_HASH_STRUCTURED_CONTENT{ - sourcemeta::core::JSON::make_object().as_object().hash( - "structuredContent")}; -const auto MCP_HASH_TEXT{ - sourcemeta::core::JSON::make_object().as_object().hash("text")}; -const auto MCP_HASH_TITLE{ - sourcemeta::core::JSON::make_object().as_object().hash("title")}; -const auto MCP_HASH_TOOLS{ - sourcemeta::core::JSON::make_object().as_object().hash("tools")}; -const auto MCP_HASH_TYPE{ - sourcemeta::core::JSON::make_object().as_object().hash("type")}; -const auto MCP_HASH_URI{ - sourcemeta::core::JSON::make_object().as_object().hash("uri")}; -const auto MCP_HASH_URI_TEMPLATE{ - sourcemeta::core::JSON::make_object().as_object().hash("uriTemplate")}; -const auto MCP_HASH_VERSION{ - sourcemeta::core::JSON::make_object().as_object().hash("version")}; -const auto MCP_HASH_WEBSITE_URL{ - sourcemeta::core::JSON::make_object().as_object().hash("websiteUrl")}; - -} // namespace - -namespace sourcemeta::one { - -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 &id, - 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(id, std::move(envelope_result)); -} - -auto mcp_make_tool_success(const MCPProtocolVersion version, - const sourcemeta::core::JSON &id, - 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(id, std::move(envelope_result)); -} - -auto mcp_make_tool_error(const sourcemeta::core::JSON &id, - 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(id, std::move(envelope_result)); -} - -auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &id, - const std::string_view uri) - -> sourcemeta::core::JSON { - return sourcemeta::core::jsonrpc_make_error(&id, 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_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 *id{sourcemeta::core::jsonrpc_request_id(request)}; - const auto *params{sourcemeta::core::jsonrpc_params(request)}; - if (id == nullptr || params == nullptr || !params->is_object()) { - return sourcemeta::core::jsonrpc_make_error_invalid_request(id); - } - - std::string_view requested_version{}; - if (params->defines("protocolVersion", MCP_HASH_PROTOCOL_VERSION) && - params->at("protocolVersion", MCP_HASH_PROTOCOL_VERSION).is_string()) { - requested_version = - params->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(*id, std::move(result)); -} - -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_tool_call_arguments(const sourcemeta::core::JSON &envelope) - -> const sourcemeta::core::JSON * { - const auto *params{sourcemeta::core::jsonrpc_params(envelope)}; - if (params == nullptr || !params->is_object() || - !params->defines("arguments", MCP_HASH_ARGUMENTS)) { - return nullptr; - } - return ¶ms->at("arguments", MCP_HASH_ARGUMENTS); -} - -} // namespace sourcemeta::one diff --git a/src/router/CMakeLists.txt b/src/router/CMakeLists.txt index aba03c554..55b3622a0 100644 --- a/src/router/CMakeLists.txt +++ b/src/router/CMakeLists.txt @@ -2,7 +2,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME router SOURCES base.cc router.cc) target_link_libraries(sourcemeta_one_router PUBLIC sourcemeta::one::http) -target_link_libraries(sourcemeta_one_router PUBLIC sourcemeta::one::mcp) +target_link_libraries(sourcemeta_one_router PUBLIC sourcemeta::core::mcp) target_link_libraries(sourcemeta_one_router PUBLIC sourcemeta::core::uritemplate) target_link_libraries(sourcemeta_one_router PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_one_router PUBLIC sourcemeta::core::jsonrpc) diff --git a/src/router/include/sourcemeta/one/router.h b/src/router/include/sourcemeta/one/router.h index 4c322f22d..d90a57790 100644 --- a/src/router/include/sourcemeta/one/router.h +++ b/src/router/include/sourcemeta/one/router.h @@ -5,10 +5,10 @@ #include #include #include +#include #include #include -#include #include // std::size_t #include // std::filesystem::path @@ -40,9 +40,10 @@ class RouterAction { virtual auto rest(const std::span matches, HTTPRequest &request, HTTPResponse &response) -> void = 0; - virtual auto - mcp(const MCPProtocolVersion version, const sourcemeta::core::JSON &id, - const sourcemeta::core::JSON &arguments, const std::string_view envelope) + virtual auto mcp(const sourcemeta::core::MCPProtocolVersion version, + const sourcemeta::core::JSON &id, + const sourcemeta::core::JSON &arguments, + const std::string_view envelope) -> sourcemeta::core::JSON = 0; [[nodiscard]] auto base() const noexcept -> const std::filesystem::path & { diff --git a/test/unit/actions/actions_schema_directory_test.cc b/test/unit/actions/actions_schema_directory_test.cc index e019afab7..6cee36187 100644 --- a/test/unit/actions/actions_schema_directory_test.cc +++ b/test/unit/actions/actions_schema_directory_test.cc @@ -17,7 +17,7 @@ class TestAction final : public sourcemeta::one::RouterAction { auto rest(const std::span, sourcemeta::one::HTTPRequest &, sourcemeta::one::HTTPResponse &) -> void override {} - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); diff --git a/test/unit/actions/actions_uri_to_relative_path_test.cc b/test/unit/actions/actions_uri_to_relative_path_test.cc index 51254a0b8..4560a38ca 100644 --- a/test/unit/actions/actions_uri_to_relative_path_test.cc +++ b/test/unit/actions/actions_uri_to_relative_path_test.cc @@ -17,7 +17,7 @@ class TestAction final : public sourcemeta::one::RouterAction { auto rest(const std::span, sourcemeta::one::HTTPRequest &, sourcemeta::one::HTTPResponse &) -> void override {} - auto mcp(const sourcemeta::one::MCPProtocolVersion, + auto mcp(const sourcemeta::core::MCPProtocolVersion, const sourcemeta::core::JSON &id, const sourcemeta::core::JSON &, const std::string_view) -> sourcemeta::core::JSON override { return sourcemeta::core::jsonrpc_make_error_method_not_found(id); diff --git a/test/unit/mcp/CMakeLists.txt b/test/unit/mcp/CMakeLists.txt deleted file mode 100644 index 8ad0ff9b0..000000000 --- a/test/unit/mcp/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -sourcemeta_googletest(NAMESPACE sourcemeta PROJECT one NAME mcp - SOURCES mcp_test.cc) - -target_link_libraries(sourcemeta_one_mcp_unit - PRIVATE sourcemeta::one::mcp) -target_link_libraries(sourcemeta_one_mcp_unit - PRIVATE sourcemeta::core::jsonrpc) -target_link_libraries(sourcemeta_one_mcp_unit - PRIVATE sourcemeta::core::json) diff --git a/test/unit/mcp/mcp_test.cc b/test/unit/mcp/mcp_test.cc deleted file mode 100644 index 2cedec7c6..000000000 --- a/test/unit/mcp/mcp_test.cc +++ /dev/null @@ -1,113 +0,0 @@ -#include - -#include - -#include - -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::one::mcp_make_tool_success( - sourcemeta::one::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::one::mcp_make_tool_success( - sourcemeta::one::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::one::mcp_make_tool_success( - sourcemeta::one::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_error_with_message) { - const auto identifier{sourcemeta::core::JSON{7}}; - const auto envelope{ - sourcemeta::one::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::one::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::one::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); -} diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 6c8c05bce..79a2302ea 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/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/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index 18a7effdb..912fd639e 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/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/vendor/core/src/core/json/construct.h b/vendor/core/src/core/json/construct.h index c2cd5674d..f7c5661bf 100644 --- a/vendor/core/src/core/json/construct.h +++ b/vendor/core/src/core/json/construct.h @@ -500,10 +500,10 @@ do_construct_object_key: { const auto key_length{key_entry.length}; if (std::memchr(key_data, '\\', key_length)) { key = internal::unescape_string(key_data, key_length); - key_hash = frames.back().get().as_object().hash(key); + key_hash = Result::Object::hash(key); } else { key.assign(key_data, key_length); - key_hash = frames.back().get().as_object().hash(key_data, key_length); + key_hash = Result::Object::hash(key_data, key_length); } key_line = key_entry.line; key_column = key_entry.column; diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h index b0e5585a1..6b2abb5dc 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h @@ -132,20 +132,20 @@ template class JSONObject { // the `Key`-accepting overload as before. /// Compute a hash for a key - [[nodiscard]] inline auto hash(const Key &key) const noexcept -> hash_type { - return this->hasher(key); + [[nodiscard]] static inline auto hash(const Key &key) noexcept -> hash_type { + return hasher(key); } /// Compute a hash for a key template requires std::same_as, KeyView> - [[nodiscard]] inline auto hash(T key) const noexcept -> hash_type { - return this->hasher(key.data(), key.size()); + [[nodiscard]] static inline auto hash(T key) noexcept -> hash_type { + return hasher(key.data(), key.size()); } /// Compute a hash from raw data - [[nodiscard]] inline auto hash(const char *raw_data, - const std::size_t raw_size) const noexcept + [[nodiscard]] static inline auto hash(const char *raw_data, + const std::size_t raw_size) noexcept -> hash_type { return hasher(raw_data, raw_size); } diff --git a/vendor/core/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h b/vendor/core/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h index 3ac86a8ae..314fafbcd 100644 --- a/vendor/core/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h +++ b/vendor/core/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/vendor/core/src/core/jsonrpc/jsonrpc.cc b/vendor/core/src/core/jsonrpc/jsonrpc.cc index 8aa10ade4..ca15a878e 100644 --- a/vendor/core/src/core/jsonrpc/jsonrpc.cc +++ b/vendor/core/src/core/jsonrpc/jsonrpc.cc @@ -2,32 +2,23 @@ #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 { -const auto JSONRPC_HASH_ID{ - sourcemeta::core::JSON::make_object().as_object().hash("id")}; +const auto JSONRPC_HASH_ID{sourcemeta::core::JSON::Object::hash("id")}; const auto JSONRPC_HASH_JSONRPC{ - sourcemeta::core::JSON::make_object().as_object().hash("jsonrpc")}; -const auto JSONRPC_HASH_METHOD{ - sourcemeta::core::JSON::make_object().as_object().hash("method")}; -const auto JSONRPC_HASH_RESULT{ - sourcemeta::core::JSON::make_object().as_object().hash("result")}; -const auto JSONRPC_HASH_ERROR{ - sourcemeta::core::JSON::make_object().as_object().hash("error")}; -const auto JSONRPC_HASH_CODE{ - sourcemeta::core::JSON::make_object().as_object().hash("code")}; + sourcemeta::core::JSON::Object::hash("jsonrpc")}; +const auto JSONRPC_HASH_METHOD{sourcemeta::core::JSON::Object::hash("method")}; +const auto JSONRPC_HASH_RESULT{sourcemeta::core::JSON::Object::hash("result")}; +const auto JSONRPC_HASH_ERROR{sourcemeta::core::JSON::Object::hash("error")}; +const auto JSONRPC_HASH_CODE{sourcemeta::core::JSON::Object::hash("code")}; const auto JSONRPC_HASH_MESSAGE{ - sourcemeta::core::JSON::make_object().as_object().hash("message")}; -const auto JSONRPC_HASH_DATA{ - sourcemeta::core::JSON::make_object().as_object().hash("data")}; -const auto JSONRPC_HASH_PARAMS{ - sourcemeta::core::JSON::make_object().as_object().hash("params")}; + sourcemeta::core::JSON::Object::hash("message")}; +const auto JSONRPC_HASH_DATA{sourcemeta::core::JSON::Object::hash("data")}; +const auto JSONRPC_HASH_PARAMS{sourcemeta::core::JSON::Object::hash("params")}; } // namespace @@ -73,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 {}; } @@ -120,13 +111,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; } @@ -137,30 +126,26 @@ 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()}; - 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/vendor/core/src/core/mcp/CMakeLists.txt b/vendor/core/src/core/mcp/CMakeLists.txt new file mode 100644 index 000000000..a76973d0e --- /dev/null +++ b/vendor/core/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/vendor/core/src/core/mcp/include/sourcemeta/core/mcp.h b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp.h new file mode 100644 index 000000000..9870afcba --- /dev/null +++ b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp.h @@ -0,0 +1,531 @@ +#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::unreachable + +/// @defgroup mcp MCP +/// @brief Helpers for building Model Context Protocol (MCP) envelopes. +/// +/// 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 + -> JSON::StringView { + 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 JSON::StringView MCP_METHOD_INITIALIZE{"initialize"}; + +/// @ingroup mcp +/// The MCP method name for the `ping` request. +constexpr JSON::StringView MCP_METHOD_PING{"ping"}; + +/// @ingroup mcp +/// The MCP method name for the `tools/list` request. +constexpr JSON::StringView MCP_METHOD_TOOLS_LIST{"tools/list"}; + +/// @ingroup mcp +/// The MCP method name for the `tools/call` request. +constexpr JSON::StringView MCP_METHOD_TOOLS_CALL{"tools/call"}; + +/// @ingroup mcp +/// The MCP method name for the `resources/list` request. +constexpr JSON::StringView MCP_METHOD_RESOURCES_LIST{"resources/list"}; + +/// @ingroup mcp +/// The MCP method name for the `resources/read` request. +constexpr JSON::StringView MCP_METHOD_RESOURCES_READ{"resources/read"}; + +/// @ingroup mcp +/// The MCP method name for the `resources/templates/list` request. +constexpr JSON::StringView MCP_METHOD_RESOURCES_TEMPLATES_LIST{ + "resources/templates/list"}; + +/// @ingroup mcp +/// The MCP method name for the `notifications/initialized` notification. +constexpr JSON::StringView 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 JSON::StringView 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, or `std::nullopt` when the value 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 JSON::StringView 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") { + 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 JSON::StringView text) -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build an MCP content block referencing a resource by URI. 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 JSON::StringView uri, + const JSON::StringView mime_type, + const JSON::StringView name = {}, + const JSON::StringView description = {}) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build a JSON-RPC envelope wrapping a successful MCP tool call response from +/// the given result payload. 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 from +/// caller-provided content blocks and a structured payload. 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 with the +/// given error message. 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 JSON::StringView message) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build a JSON-RPC error envelope reporting that an MCP resource URI could +/// not be resolved. 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 JSON::StringView uri) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Build an MCP resource descriptor as used in `resources/list` responses. 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 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; + +/// @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 JSON::StringView uri, + const JSON::StringView mime_type, + const JSON::StringView 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 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. + JSON::StringView 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. 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 JSON::StringView name, + const JSON::StringView 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. +struct MCPImplementation { + /// Short machine-readable server name. + JSON::StringView name; + /// Semver-compatible server version. + JSON::StringView version; + /// Optional human-readable title. + JSON::StringView title = {}; + /// Optional human-readable description. + JSON::StringView description = {}; + /// Optional public website URL. + JSON::StringView website_url = {}; +}; + +/// @ingroup mcp +/// Boolean toggles for the MCP `capabilities` object returned during the +/// initialize handshake. +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. 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 JSON::StringView instructions = {}) + -> sourcemeta::core::JSON; + +/// @ingroup mcp +/// Borrow the `arguments` object from a JSON-RPC `tools/call` envelope, or +/// return `nullptr` if no `arguments` object is present. 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/vendor/core/src/core/mcp/mcp.cc b/vendor/core/src/core/mcp/mcp.cc new file mode 100644 index 000000000..1fc8b5524 --- /dev/null +++ b/vendor/core/src/core/mcp/mcp.cc @@ -0,0 +1,382 @@ +#include + +#include +#include + +#include // assert +#include // std::size_t +#include // std::optional +#include // std::ostringstream +#include // std::string +#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 JSON::StringView text) + -> sourcemeta::core::JSON { + auto block{sourcemeta::core::JSON::make_object()}; + block.assign_assume_new("type", sourcemeta::core::JSON{"text"}, + MCP_HASH_TYPE); + block.assign_assume_new("text", sourcemeta::core::JSON{text}, MCP_HASH_TEXT); + return block; +} + +auto mcp_make_resource_link(const MCPProtocolVersion version, + 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; + 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("type", sourcemeta::core::JSON{"resource_link"}, + MCP_HASH_TYPE); + block.assign_assume_new("uri", sourcemeta::core::JSON{uri}, MCP_HASH_URI); + if (!name.empty()) { + block.assign_assume_new("name", sourcemeta::core::JSON{name}, + MCP_HASH_NAME); + } + if (!description.empty()) { + block.assign_assume_new("description", sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); + } + block.assign_assume_new("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("content", std::move(content), + MCP_HASH_CONTENT); + if (mcp_supports_structured_content(version)) { + envelope_result.assign_assume_new("structuredContent", std::move(result), + MCP_HASH_STRUCTURED_CONTENT); + } + 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)); +} + +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("content", std::move(content_blocks), + MCP_HASH_CONTENT); + if (mcp_supports_structured_content(version)) { + envelope_result.assign_assume_new("structuredContent", + std::move(structured), + MCP_HASH_STRUCTURED_CONTENT); + } + 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)); +} + +auto mcp_make_tool_error(const sourcemeta::core::JSON &identifier, + const JSON::StringView 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("content", std::move(content), + MCP_HASH_CONTENT); + 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)); +} + +auto mcp_make_error_resource_not_found(const sourcemeta::core::JSON &identifier, + 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 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()}; + 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("description", + sourcemeta::core::JSON{description}, + MCP_HASH_DESCRIPTION); + } + resource.assign_assume_new("mimeType", sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + if (size.has_value()) { + resource.assign_assume_new("size", sourcemeta::core::JSON{size.value()}, + MCP_HASH_SIZE); + } + return resource; +} + +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); + entry.assign_assume_new("mimeType", sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + 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("contents", std::move(contents), MCP_HASH_CONTENTS); + return result; +} + +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}, + MCP_HASH_URI_TEMPLATE); + 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("mimeType", sourcemeta::core::JSON{mime_type}, + MCP_HASH_MIME_TYPE); + return entry; +} + +auto mcp_make_tool_descriptor( + 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); + assert(!annotations.read_only || annotations.idempotent); + + auto entry{sourcemeta::core::JSON::make_object()}; + 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("inputSchema", std::move(input_schema), + MCP_HASH_INPUT_SCHEMA); + if (output_schema.has_value() && mcp_supports_output_schema(version)) { + 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( + "title", sourcemeta::core::JSON{annotations.title}, MCP_HASH_TITLE); + } + annotations_object.assign_assume_new( + "readOnlyHint", sourcemeta::core::JSON{annotations.read_only}, + MCP_HASH_READ_ONLY_HINT); + annotations_object.assign_assume_new( + "destructiveHint", sourcemeta::core::JSON{annotations.destructive}, + MCP_HASH_DESTRUCTIVE_HINT); + annotations_object.assign_assume_new( + "idempotentHint", sourcemeta::core::JSON{annotations.idempotent}, + MCP_HASH_IDEMPOTENT_HINT); + annotations_object.assign_assume_new( + "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; +} + +auto mcp_make_initialize_result(const sourcemeta::core::JSON &request, + const MCPServerCapabilities &capabilities, + const MCPImplementation &server, + const JSON::StringView 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); + } + + JSON::StringView requested_version{}; + 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)}; + + auto capabilities_object{sourcemeta::core::JSON::make_object()}; + if (capabilities.prompts) { + capabilities_object.assign_assume_new( + "prompts", sourcemeta::core::JSON::make_object(), MCP_HASH_PROMPTS); + } + if (capabilities.resources) { + capabilities_object.assign_assume_new( + "resources", sourcemeta::core::JSON::make_object(), MCP_HASH_RESOURCES); + } + if (capabilities.tools) { + capabilities_object.assign_assume_new( + "tools", sourcemeta::core::JSON::make_object(), MCP_HASH_TOOLS); + } + if (capabilities.logging) { + capabilities_object.assign_assume_new( + "logging", sourcemeta::core::JSON::make_object(), MCP_HASH_LOGGING); + } + if (capabilities.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( + "version", sourcemeta::core::JSON{server.version}, MCP_HASH_VERSION); + if (!server.title.empty() && mcp_supports_implementation_title(version)) { + 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("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("websiteUrl", + sourcemeta::core::JSON{server.website_url}, + MCP_HASH_WEBSITE_URL); + } + + auto result{sourcemeta::core::JSON::make_object()}; + result.assign_assume_new( + "protocolVersion", + sourcemeta::core::JSON{mcp_protocol_version_string(version)}, + MCP_HASH_PROTOCOL_VERSION); + result.assign_assume_new("capabilities", std::move(capabilities_object), + MCP_HASH_CAPABILITIES); + result.assign_assume_new("serverInfo", std::move(server_info), + MCP_HASH_SERVER_INFO); + if (!instructions.empty()) { + result.assign_assume_new("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()) { + return nullptr; + } + const auto *arguments{parameters->try_at("arguments", MCP_HASH_ARGUMENTS)}; + if (arguments == nullptr || !arguments->is_object()) { + return nullptr; + } + return arguments; +} + +} // namespace sourcemeta::core