From 81d3d509669afe7a3e462bc8c9f4aa976e9bf93d Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 28 Aug 2026 10:11:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=87=BAmcp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 3 +- kernel/main.cmake | 5 +- mcp/core/Control_Service.cpp | 278 +++++++++++++++ mcp/core/Control_Service.hpp | 47 +++ mcp/core/Control_Service.ipp | 11 + mcp/core/Protocol_Type.hpp | 39 +++ mcp/core/Protocol_Type.ipp | 330 ++++++++++++++++++ mcp/core/Render_Types.hpp | 14 + mcp/core/Render_Types.ipp | 27 ++ .../core/runtime}/Gallery_Plots.cpp | 0 .../core/runtime}/Gallery_Plots.hpp | 0 .../core/runtime}/Gallery_Plots_2D.cpp | 2 +- .../core/runtime}/Gallery_Plots_2D.hpp | 0 .../core/runtime}/Gallery_Plots_3D.cpp | 2 +- .../core/runtime}/Gallery_Plots_3D.hpp | 0 {web_server/src => mcp/core/runtime}/Plot.cpp | 0 {web_server/src => mcp/core/runtime}/Plot.hpp | 0 .../core/runtime}/Renderable_Adapter.hpp | 4 +- .../core/runtime}/Renderable_Adapter.ipp | 181 +++------- .../core/runtime}/Taskflow_Trace_Json.hpp | 0 mcp/main.cmake | 92 +++++ mcp/server/Mcp_Server.cpp | 147 ++++++++ mcp/server/Mcp_Server.hpp | 8 + mcp/server/main.cpp | 32 ++ mcp/tests/Protocol_Type_Tests.cpp | 72 ++++ web_server/main.cmake | 1 + web_server/src/Gallery_Video_Stream.hpp | 2 +- web_server/src/Graph_WebSocket.hpp | 2 +- web_server/src/Web_Server.cpp | 112 ++---- web_server/src/detail/Gallery_Frame_Atlas.hpp | 2 +- .../src/frame_sampling/Frame_Sampler.hpp | 2 +- web_server/tests/Gallery_Plots_Tests.cpp | 2 +- 32 files changed, 1179 insertions(+), 238 deletions(-) create mode 100644 mcp/core/Control_Service.cpp create mode 100644 mcp/core/Control_Service.hpp create mode 100644 mcp/core/Control_Service.ipp create mode 100644 mcp/core/Protocol_Type.hpp create mode 100644 mcp/core/Protocol_Type.ipp create mode 100644 mcp/core/Render_Types.hpp create mode 100644 mcp/core/Render_Types.ipp rename {web_server/src => mcp/core/runtime}/Gallery_Plots.cpp (100%) rename {web_server/src => mcp/core/runtime}/Gallery_Plots.hpp (100%) rename {web_server/src => mcp/core/runtime}/Gallery_Plots_2D.cpp (99%) rename {web_server/src => mcp/core/runtime}/Gallery_Plots_2D.hpp (100%) rename {web_server/src => mcp/core/runtime}/Gallery_Plots_3D.cpp (99%) rename {web_server/src => mcp/core/runtime}/Gallery_Plots_3D.hpp (100%) rename {web_server/src => mcp/core/runtime}/Plot.cpp (100%) rename {web_server/src => mcp/core/runtime}/Plot.hpp (100%) rename {web_server/src => mcp/core/runtime}/Renderable_Adapter.hpp (94%) rename {web_server/src => mcp/core/runtime}/Renderable_Adapter.ipp (67%) rename {web_server/src => mcp/core/runtime}/Taskflow_Trace_Json.hpp (100%) create mode 100644 mcp/main.cmake create mode 100644 mcp/server/Mcp_Server.cpp create mode 100644 mcp/server/Mcp_Server.hpp create mode 100644 mcp/server/main.cpp create mode 100644 mcp/tests/Protocol_Type_Tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d3a08fc..8a4196f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,5 +8,6 @@ include("${CMAKE_CURRENT_LIST_DIR}/cmake/package.cmake") include("${CMAKE_CURRENT_LIST_DIR}/kernel/main.cmake") include("${CMAKE_CURRENT_LIST_DIR}/render_2D/main.cmake") include("${CMAKE_CURRENT_LIST_DIR}/render_3D/main.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/mcp/main.cmake") include("${CMAKE_CURRENT_LIST_DIR}/web_server/main.cmake") -include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/end.cmake") \ No newline at end of file +include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/end.cmake") diff --git a/kernel/main.cmake b/kernel/main.cmake index 5ace37a..16ffb66 100644 --- a/kernel/main.cmake +++ b/kernel/main.cmake @@ -3,7 +3,7 @@ set(Aethera_Kernel_dependencies aethera_kernel::taskflow global::magic_enum glob set(Aethera_BUILD_TESTS TRUE) if (Aethera_BUILD_TESTS) set(Aethera_Kernel_test_targets) - list(APPEND Aethera_Kernel_dependencies global::GTest) + list(APPEND Aethera_Kernel_dependencies global::GTest global::benchmark) endif () rcl_add_dependency_action_targets(Aethera_Kernel_env ${Aethera_Kernel_dependencies}) set_target_properties(Aethera_Kernel_env PROPERTIES FOLDER Aethera_Kernel) @@ -19,6 +19,7 @@ find_package(magic_enum CONFIG REQUIRED) find_package(tl-expected CONFIG REQUIRED) if (Aethera_BUILD_TESTS) find_package(GTest CONFIG REQUIRED) + find_package(benchmark CONFIG REQUIRED) endif () set(Aethera_Kernel_source_dir "${CMAKE_CURRENT_LIST_DIR}/src/kernel") set(Aethera_concurrentqueue_dir "${CMAKE_CURRENT_LIST_DIR}/third_party/concurrentqueue-1.0.5") @@ -48,7 +49,7 @@ if (Aethera_BUILD_TESTS) set(Aethera_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/src/test") append_glob_source(Aethera_Kernel_test_sources "${Aethera_Kernel_test_dir}") add_executable(Aethera_Kernel_exe ${Aethera_Kernel_test_sources}) - target_link_libraries(Aethera_Kernel_exe PRIVATE Aethera_Kernel GTest::gtest) + target_link_libraries(Aethera_Kernel_exe PRIVATE Aethera_Kernel GTest::gtest benchmark::benchmark) add_test(NAME Aethera_Kernel_exe COMMAND Aethera_Kernel_exe) set_tests_properties(Aethera_Kernel_exe PROPERTIES LABELS "Aethera_Kernel" diff --git a/mcp/core/Control_Service.cpp b/mcp/core/Control_Service.cpp new file mode 100644 index 0000000..e39d96e --- /dev/null +++ b/mcp/core/Control_Service.cpp @@ -0,0 +1,278 @@ +#include "Control_Service.hpp" +#include "Control_Service.ipp" +#include "Protocol_Type.hpp" +#include "runtime/Gallery_Plots.hpp" +#include +#include +#include +#include + +namespace aethera::mcp { +namespace { + +struct Empty_Request {}; +struct Plot_Request { + std::string plot; /* Gallery Plot 稳定标识。 */ +}; +struct Component_Request { + std::string plot; /* Gallery Plot 稳定标识。 */ + std::string component; /* Renderable 业务组件标识。 */ +}; +struct Write_Property_Request { + std::string plot; /* Gallery Plot 稳定标识。 */ + std::string component; /* Renderable 业务组件标识。 */ + std::string property; /* 待修改属性键。 */ + nlohmann::json value; /* 属性协议值。 */ +}; +struct Generate_Data_Request { + std::string plot; /* Gallery Plot 稳定标识。 */ + nlohmann::json input; /* Plot 数据生成器输入。 */ +}; + +using Invoke = Tool_Call_Output (*)(Control_Service&, const nlohmann::json&); +using Schema = nlohmann::json (*)(); + +struct Operation { + std::string_view name; /* MCP 稳定 tool name。 */ + std::string_view description; /* 模型选择工具时使用的业务说明。 */ + Schema schema; /* PFR 自动生成的输入结构。 */ + Invoke invoke; /* 协议无关的业务调用入口。 */ +}; + +template +[[nodiscard]] nlohmann::json request_schema() { + return describe_protocol_type(); +} + +template +[[nodiscard]] Tool_Call_Output decode_and_call( + const nlohmann::json& arguments, Callback&& callback) { + try { + Request request{}; + decode_protocol_value(request, arguments); + return std::forward(callback)(request); + } catch (const nlohmann::json::exception& failure) { + return {Tool_Call_Result::invalid_arguments, {}, failure.what()}; + } catch (const std::invalid_argument& failure) { + return {Tool_Call_Result::invalid_arguments, {}, failure.what()}; + } +} + +[[nodiscard]] Tool_Call_Output list_plots( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto&) { + return Tool_Call_Output{Tool_Call_Result::ok, service.plot_catalog(), {}}; + }); +} + +[[nodiscard]] Tool_Call_Output plot_schema( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto& request) { + const auto plot = service.find_plot(request.plot); + if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + return Tool_Call_Output{Tool_Call_Result::ok, plot->schema(), {}}; + }); +} + +[[nodiscard]] Tool_Call_Output plot_diagnostics( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto& request) { + const auto plot = service.find_plot(request.plot); + if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + return Tool_Call_Output{Tool_Call_Result::ok, plot->diagnostics(), {}}; + }); +} + +[[nodiscard]] Tool_Call_Output reset_plot_diagnostics( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto& request) { + const auto plot = service.find_plot(request.plot); + if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + plot->reset_diagnostics(); + return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}}, {}}; + }); +} + +[[nodiscard]] Tool_Call_Output component_state( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto& request) { + const auto plot = service.find_plot(request.plot); + if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + auto result = plot->component_state(request.component); + if (!result.value("success", true)) + return Tool_Call_Output{Tool_Call_Result::rejected, std::move(result), "unknown component"}; + return Tool_Call_Output{Tool_Call_Result::ok, std::move(result), {}}; + }); +} + +[[nodiscard]] Tool_Call_Output write_property( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto& request) { + const auto plot = service.find_plot(request.plot); + if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + auto result = plot->write_prop( + request.component, request.property, request.value); + const bool success = result.value("success", false); + return Tool_Call_Output{success ? Tool_Call_Result::ok : Tool_Call_Result::rejected, + std::move(result), success ? "" : "property write rejected"}; + }); +} + +[[nodiscard]] Tool_Call_Output generate_data( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto& request) { + const auto plot = service.find_plot(request.plot); + if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + return Tool_Call_Output{Tool_Call_Result::ok, + plot->generate_data(request.input), {}}; + }); +} + +[[nodiscard]] Tool_Call_Output begin_benchmark( + Control_Service& service, const nlohmann::json& arguments) { + return decode_and_call(arguments, [&service](const auto& request) { + const auto plot = service.find_plot(request.plot); + if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"}; + plot->reset_diagnostics(); + plot->render_once(); + return Tool_Call_Output{Tool_Call_Result::ok, + {{"accepted", true}, {"plot", request.plot}, + {"status_tool", "aethera_benchmark_read"}}, {}}; + }); +} + +[[nodiscard]] nlohmann::json task_runtime_json() { + const auto state = task_runtime_state(); + nlohmann::json workers = nlohmann::json::array(); + for (const auto& worker : state.workers) + workers.push_back({ + {"id", worker.id}, {"task_count", worker.task_count}, + {"current_queue_size", worker.current_queue_size}, + {"current_queue_capacity", worker.current_queue_capacity}, + {"peak_queue_size", worker.peak_observed_queue_size}, + {"max_queue_capacity", worker.max_observed_queue_capacity}, + {"active_task", {{"native_id", std::to_string(worker.active_task_hash)}, + {"type", worker.active_task_type}, + {"time_ns", worker.active_task_time_ns}}}, + {"task_time_ns", worker.task_time_ns}, + {"busy_time_ns", worker.busy_time_ns}, + {"cpu_time_ns", worker.cpu_time_ns}, + {"non_cpu_time_ns", worker.non_cpu_time_ns}, + {"idle_time_ns", worker.idle_time_ns}, + {"min_task_time_ns", worker.min_task_time_ns}, + {"max_task_time_ns", worker.max_task_time_ns}, + {"utilization", worker.utilization}, + {"cpu_utilization", worker.cpu_utilization}}); + nlohmann::json task_types = nlohmann::json::array(); + for (const auto& type : state.task_types) + task_types.push_back({ + {"name", type.name}, {"count", type.count}, + {"total_time_ns", type.total_time_ns}, + {"min_time_ns", type.min_time_ns}, + {"max_time_ns", type.max_time_ns}}); + return { + {"protocol", "aethera.taskflow.runtime"}, {"version", 1}, + {"worker_count", state.worker_count}, + {"active_topologies", state.active_topology_count}, + {"active_taskflows", state.active_taskflow_count}, + {"peak_active_taskflows", state.peak_active_taskflow_count}, + {"completed_taskflows", state.completed_taskflow_count}, + {"failed_taskflows", state.failed_taskflow_count}, + {"active_tasks", state.active_task_count}, + {"peak_active_tasks", state.peak_active_task_count}, + {"active_workers", state.active_worker_count}, + {"peak_active_workers", state.peak_active_worker_count}, + {"observed_tasks", state.observed_task_count}, + {"named_tasks", state.named_task_count}, + {"peak_worker_queue_size", state.peak_observed_worker_queue_size}, + {"max_worker_queue_capacity", state.max_observed_worker_queue_capacity}, + {"longest_task", {{"native_id", std::to_string(state.longest_task_hash)}, + {"name", state.longest_task_name}, + {"type", state.longest_task_type}, + {"time_ns", state.longest_task_time_ns}}}, + {"total_task_time_ns", state.total_task_time_ns}, + {"worker_busy_time_ns", state.worker_busy_time_ns}, + {"worker_cpu_time_ns", state.worker_cpu_time_ns}, + {"observed_wall_time_ns", state.observed_wall_time_ns}, + {"worker_utilization", state.worker_utilization}, + {"worker_cpu_utilization", state.worker_cpu_utilization}, + {"task_types", std::move(task_types)}, {"workers", std::move(workers)}}; +} + +[[nodiscard]] Tool_Call_Output task_runtime( + Control_Service&, const nlohmann::json& arguments) { + return decode_and_call(arguments, [](const auto&) { + return Tool_Call_Output{Tool_Call_Result::ok, task_runtime_json(), {}}; + }); +} + +constexpr std::array operations{ + Operation{"aethera_plot_list", "List every 2D and 3D gallery plot.", + &request_schema, &list_plots}, + Operation{"aethera_plot_schema", "Describe a plot and its editable render components.", + &request_schema, &plot_schema}, + Operation{"aethera_plot_diagnostics", "Read current frame, scene, GPU and Datoviz diagnostics.", + &request_schema, &plot_diagnostics}, + Operation{"aethera_plot_diagnostics_reset", "Reset the plot's authoritative diagnostic counters.", + &request_schema, &reset_plot_diagnostics}, + Operation{"aethera_component_state", "Read one render component's published state.", + &request_schema, &component_state}, + Operation{"aethera_component_write", "Write one editable render component property.", + &request_schema, &write_property}, + Operation{"aethera_data_generate", "Generate input data for a gallery plot.", + &request_schema, &generate_data}, + Operation{"aethera_benchmark_begin", "Reset diagnostics and asynchronously start rendering a plot.", + &request_schema, &begin_benchmark}, + Operation{"aethera_benchmark_read", "Read benchmark results from the plot's current diagnostics.", + &request_schema, &plot_diagnostics}, + Operation{"aethera_task_runtime", "Read Taskflow executor utilization and queue diagnostics.", + &request_schema, &task_runtime}, +}; + +} + +Control_Service::Control_Service() : d(std::make_unique()) {} +Control_Service::~Control_Service() = default; + +std::shared_ptr Control_Service::create() { + auto service = std::shared_ptr(new Control_Service); + service->d->plots.reserve(web::gallery_plot_definitions().size()); + for (const auto& definition : web::gallery_plot_definitions()) + service->d->plots.emplace(definition.id, definition.create()); + return service; +} + +std::shared_ptr Control_Service::find_plot( + std::string_view id) const { + const auto found = d->plots.find(std::string{id}); + return found == d->plots.end() ? nullptr : found->second; +} + +nlohmann::json Control_Service::plot_catalog() const { + nlohmann::json result = nlohmann::json::array(); + for (const auto& definition : web::gallery_plot_definitions()) + result.push_back({ + {"id", definition.id}, {"title", definition.title}, + {"category", definition.category}, + {"description", definition.description}, + {"dimension", web::plot_dimension_name(definition.dimension)}}); + return result; +} + +nlohmann::json Control_Service::tool_catalog() const { + nlohmann::json result = nlohmann::json::array(); + for (const auto& operation : operations) + result.push_back({{"name", operation.name}, + {"description", operation.description}, + {"inputSchema", operation.schema()}}); + return result; +} + +Tool_Call_Output Control_Service::call_tool( + std::string_view name, const nlohmann::json& arguments) { + for (const auto& operation : operations) + if (operation.name == name) return operation.invoke(*this, arguments); + return {Tool_Call_Result::unknown_tool, {}, "unknown tool"}; +} + +} diff --git a/mcp/core/Control_Service.hpp b/mcp/core/Control_Service.hpp new file mode 100644 index 0000000..ccc1eab --- /dev/null +++ b/mcp/core/Control_Service.hpp @@ -0,0 +1,47 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace aethera::web { +struct Plot; +} + +namespace aethera::mcp { + +enum struct Tool_Call_Result : std::uint8_t { + ok, + unknown_tool, + invalid_arguments, + unknown_plot, + rejected +}; + +struct Tool_Call_Output { + Tool_Call_Result result{Tool_Call_Result::ok}; /* 调用的已知业务结果。 */ + nlohmann::json content{}; /* 成功值或结构化错误细节。 */ + std::string message{}; /* 供协议适配器显示的简短说明。 */ +}; + +struct Control_Service final { + static std::shared_ptr create(); + ~Control_Service(); + Control_Service(const Control_Service&) = delete; + Control_Service& operator=(const Control_Service&) = delete; + + [[nodiscard]] std::shared_ptr find_plot( + std::string_view id) const; + [[nodiscard]] nlohmann::json plot_catalog() const; + [[nodiscard]] nlohmann::json tool_catalog() const; + [[nodiscard]] Tool_Call_Output call_tool( + std::string_view name, const nlohmann::json& arguments); + +private: + Control_Service(); + struct Private; + std::unique_ptr d; +}; + +} diff --git a/mcp/core/Control_Service.ipp b/mcp/core/Control_Service.ipp new file mode 100644 index 0000000..85c8222 --- /dev/null +++ b/mcp/core/Control_Service.ipp @@ -0,0 +1,11 @@ +#pragma once +#include "runtime/Plot.hpp" +#include + +namespace aethera::mcp { + +struct Control_Service::Private { + std::unordered_map> plots; /* Plot 唯一实例注册表。 */ +}; + +} diff --git a/mcp/core/Protocol_Type.hpp b/mcp/core/Protocol_Type.hpp new file mode 100644 index 0000000..998b494 --- /dev/null +++ b/mcp/core/Protocol_Type.hpp @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include + +namespace aethera::mcp { + +template +struct Type_Tag { + using Type = std::remove_cvref_t; +}; + +template struct Boolean_Type; +template struct Integer_Type; +template struct Number_Type; +template struct Text_Type; +template struct Enum_Type; +template struct Pair_Type; +template struct Fixed_Array_Type; +template struct Sequence_Type; +template struct Aggregate_Type; +struct Json_Value_Type; + +template +[[nodiscard]] nlohmann::json encode_protocol_value(const T& value); + +template +void decode_protocol_value(T& value, const nlohmann::json& input); + +template +[[nodiscard]] nlohmann::json describe_protocol_type(); + +template +[[nodiscard]] constexpr std::string_view protocol_editor() noexcept; + +} + +#include "Protocol_Type.ipp" diff --git a/mcp/core/Protocol_Type.ipp b/mcp/core/Protocol_Type.ipp new file mode 100644 index 0000000..4f4fc1b --- /dev/null +++ b/mcp/core/Protocol_Type.ipp @@ -0,0 +1,330 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::mcp { + +template +concept Boolean = std::same_as, bool>; + +template +concept Integer = std::integral> && !Boolean && + !std::same_as, char>; + +template +concept Number = std::floating_point>; + +template +concept Text = std::same_as, std::string> || + std::same_as, char>; + +template +concept Enum = std::is_enum_v>; + +template +struct Is_Pair : std::false_type {}; + +template +struct Is_Pair> : std::true_type {}; + +template +concept Pair = Is_Pair>::value; + +template +struct Is_Fixed_Array : std::false_type {}; + +template +struct Is_Fixed_Array> : std::true_type {}; + +template +concept Fixed_Array = Is_Fixed_Array>::value; + +template +concept Sequence = std::ranges::range && + requires(T value, typename T::value_type item) { + value.clear(); + value.emplace_back(std::move(item)); + } && !Text && !Fixed_Array; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Boolean_Type; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Integer_Type; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Number_Type; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Text_Type; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Enum_Type; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Pair_Type; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Fixed_Array_Type; + +template +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Sequence_Type; + +template + requires std::is_aggregate_v> && + (!Boolean) && (!Integer) && (!Number) && (!Text) && + (!Enum) && (!Pair) && (!Fixed_Array) && (!Sequence) +[[nodiscard]] auto match_protocol_type(Type_Tag) -> Aggregate_Type; + +[[nodiscard]] auto match_protocol_type( + Type_Tag) -> Json_Value_Type; + +template +using Matched_Protocol_Type = decltype( + match_protocol_type(Type_Tag>{})); + +struct Json_Value_Type { + [[nodiscard]] static nlohmann::json encode(const nlohmann::json& value) { + return value; + } + static void decode(nlohmann::json& value, const nlohmann::json& input) { + value = input; + } + [[nodiscard]] static nlohmann::json describe() { return {}; } + [[nodiscard]] static constexpr std::string_view editor() noexcept { + return "json"; + } +}; + +namespace detail { + +template +[[nodiscard]] nlohmann::json encode_aggregate( + const T& value, std::index_sequence) { + nlohmann::json result = nlohmann::json::object(); + ((result[std::string(boost::pfr::get_name())] = + encode_protocol_value(boost::pfr::get(value))), ...); + return result; +} + +template +void decode_aggregate(T& value, const nlohmann::json& input, + std::index_sequence) { + (decode_protocol_value( + boost::pfr::get(value), + input.at(std::string(boost::pfr::get_name()))), ...); +} + +template +[[nodiscard]] nlohmann::json describe_aggregate( + std::index_sequence) { + nlohmann::json properties = nlohmann::json::object(); + nlohmann::json required = nlohmann::json::array(); + ((properties[std::string(boost::pfr::get_name())] = + describe_protocol_type(std::declval()))>>(), + required.push_back(std::string(boost::pfr::get_name()))), ...); + return {{"type", "object"}, {"properties", std::move(properties)}, + {"required", std::move(required)}, + {"additionalProperties", false}}; +} + +} + +template +struct Boolean_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { return value; } + static void decode(T& value, const nlohmann::json& input) { + value = input.template get(); + } + [[nodiscard]] static nlohmann::json describe() { return {{"type", "boolean"}}; } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "boolean"; } +}; + +template +struct Integer_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { return value; } + static void decode(T& value, const nlohmann::json& input) { + value = input.template get(); + } + [[nodiscard]] static nlohmann::json describe() { return {{"type", "integer"}}; } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "integer"; } +}; + +template +struct Number_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { return value; } + static void decode(T& value, const nlohmann::json& input) { + value = input.template get(); + } + [[nodiscard]] static nlohmann::json describe() { return {{"type", "number"}}; } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "number"; } +}; + +template +struct Text_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { + if constexpr (std::same_as) return std::string(1, value); + return value; + } + static void decode(T& value, const nlohmann::json& input) { + if constexpr (std::same_as) { + const auto text = input.template get(); + if (text.size() != 1) + throw std::invalid_argument( + "character value must contain exactly one character"); + value = text.front(); + } else { + value = input.template get(); + } + } + [[nodiscard]] static nlohmann::json describe() { + nlohmann::json result{{"type", "string"}}; + if constexpr (std::same_as) { + result["minLength"] = 1; + result["maxLength"] = 1; + } + return result; + } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "text"; } +}; + +template +struct Enum_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { + return magic_enum::enum_name(value); + } + static void decode(T& value, const nlohmann::json& input) { + const auto decoded = magic_enum::enum_cast( + input.template get()); + if (!decoded) throw std::invalid_argument("unknown enum value"); + value = *decoded; + } + [[nodiscard]] static nlohmann::json describe() { + nlohmann::json values = nlohmann::json::array(); + for (const auto value : magic_enum::enum_values()) + values.push_back(magic_enum::enum_name(value)); + return {{"type", "string"}, {"enum", std::move(values)}}; + } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "select"; } +}; + +template +struct Pair_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { + return nlohmann::json::array({encode_protocol_value(value.first), + encode_protocol_value(value.second)}); + } + static void decode(T& value, const nlohmann::json& input) { + if (!input.is_array() || input.size() != 2) + throw std::invalid_argument("pair value must be a two-item array"); + decode_protocol_value(value.first, input[0]); + decode_protocol_value(value.second, input[1]); + } + [[nodiscard]] static nlohmann::json describe() { + return {{"type", "array"}, {"minItems", 2}, {"maxItems", 2}, + {"prefixItems", nlohmann::json::array({ + describe_protocol_type(), + describe_protocol_type()})}}; + } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; } +}; + +template +struct Fixed_Array_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { + nlohmann::json result = nlohmann::json::array(); + for (const auto& item : value) + result.push_back(encode_protocol_value(item)); + return result; + } + static void decode(T& value, const nlohmann::json& input) { + if (!input.is_array() || input.size() != value.size()) + throw std::invalid_argument("array has the wrong size"); + for (std::size_t index = 0; index < value.size(); ++index) + decode_protocol_value(value[index], input[index]); + } + [[nodiscard]] static nlohmann::json describe() { + constexpr auto size = std::tuple_size_v; + return {{"type", "array"}, + {"items", describe_protocol_type()}, + {"minItems", size}, {"maxItems", size}}; + } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; } +}; + +template +struct Sequence_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { + nlohmann::json result = nlohmann::json::array(); + for (const auto& item : value) + result.push_back(encode_protocol_value(item)); + return result; + } + static void decode(T& value, const nlohmann::json& input) { + if (!input.is_array()) + throw std::invalid_argument("value must be an array"); + T updated; + for (const auto& encoded : input) { + typename T::value_type item{}; + decode_protocol_value(item, encoded); + updated.emplace_back(std::move(item)); + } + value = std::move(updated); + } + [[nodiscard]] static nlohmann::json describe() { + return {{"type", "array"}, + {"items", describe_protocol_type()}}; + } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; } +}; + +template +struct Aggregate_Type { + [[nodiscard]] static nlohmann::json encode(const T& value) { + return detail::encode_aggregate( + value, std::make_index_sequence>{}); + } + static void decode(T& value, const nlohmann::json& input) { + if (!input.is_object()) + throw std::invalid_argument("value must be an object"); + detail::decode_aggregate( + value, input, + std::make_index_sequence>{}); + } + [[nodiscard]] static nlohmann::json describe() { + return detail::describe_aggregate( + std::make_index_sequence>{}); + } + [[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; } +}; + +template +nlohmann::json encode_protocol_value(const T& value) { + return Matched_Protocol_Type::encode(value); +} + +template +void decode_protocol_value(T& value, const nlohmann::json& input) { + Matched_Protocol_Type::decode(value, input); +} + +template +nlohmann::json describe_protocol_type() { + return Matched_Protocol_Type::describe(); +} + +template +constexpr std::string_view protocol_editor() noexcept { + return Matched_Protocol_Type::editor(); +} + +} diff --git a/mcp/core/Render_Types.hpp b/mcp/core/Render_Types.hpp new file mode 100644 index 0000000..170e3c2 --- /dev/null +++ b/mcp/core/Render_Types.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "Protocol_Type.hpp" +#include + +namespace aethera::mcp { + +struct Color_Map_Type; + +[[nodiscard]] auto match_protocol_type( + Type_Tag) -> Color_Map_Type; + +} + +#include "Render_Types.ipp" diff --git a/mcp/core/Render_Types.ipp b/mcp/core/Render_Types.ipp new file mode 100644 index 0000000..083126c --- /dev/null +++ b/mcp/core/Render_Types.ipp @@ -0,0 +1,27 @@ +#pragma once +#include + +namespace aethera::mcp { + +struct Color_Map_Type { + [[nodiscard]] static nlohmann::json encode( + const render_2d::Color_Map& value) { + return {{"stops", encode_protocol_value(value.stops)}}; + } + static void decode(render_2d::Color_Map& value, + const nlohmann::json& input) { + decode_protocol_value(value.stops, input.at("stops")); + } + [[nodiscard]] static nlohmann::json describe() { + return {{"type", "object"}, + {"properties", {{"stops", + describe_protocol_type>()}}}, + {"required", nlohmann::json::array({"stops"})}, + {"additionalProperties", false}}; + } + [[nodiscard]] static constexpr std::string_view editor() noexcept { + return "color-map"; + } +}; + +} diff --git a/web_server/src/Gallery_Plots.cpp b/mcp/core/runtime/Gallery_Plots.cpp similarity index 100% rename from web_server/src/Gallery_Plots.cpp rename to mcp/core/runtime/Gallery_Plots.cpp diff --git a/web_server/src/Gallery_Plots.hpp b/mcp/core/runtime/Gallery_Plots.hpp similarity index 100% rename from web_server/src/Gallery_Plots.hpp rename to mcp/core/runtime/Gallery_Plots.hpp diff --git a/web_server/src/Gallery_Plots_2D.cpp b/mcp/core/runtime/Gallery_Plots_2D.cpp similarity index 99% rename from web_server/src/Gallery_Plots_2D.cpp rename to mcp/core/runtime/Gallery_Plots_2D.cpp index a2c6b52..0e6dc9a 100644 --- a/web_server/src/Gallery_Plots_2D.cpp +++ b/mcp/core/runtime/Gallery_Plots_2D.cpp @@ -69,7 +69,7 @@ public: return item->id() == component; }); if (found != descriptors.end()) - result[component] = (*found)->snapshot(); + result[component] = (*found)->values(); } return result; } diff --git a/web_server/src/Gallery_Plots_2D.hpp b/mcp/core/runtime/Gallery_Plots_2D.hpp similarity index 100% rename from web_server/src/Gallery_Plots_2D.hpp rename to mcp/core/runtime/Gallery_Plots_2D.hpp diff --git a/web_server/src/Gallery_Plots_3D.cpp b/mcp/core/runtime/Gallery_Plots_3D.cpp similarity index 99% rename from web_server/src/Gallery_Plots_3D.cpp rename to mcp/core/runtime/Gallery_Plots_3D.cpp index b771248..fb30880 100644 --- a/web_server/src/Gallery_Plots_3D.cpp +++ b/mcp/core/runtime/Gallery_Plots_3D.cpp @@ -372,7 +372,7 @@ public: return descriptor->id() == component; }); if (found != descriptors_.end()) - result[component] = (*found)->snapshot(); + result[component] = (*found)->values(); } return result; } diff --git a/web_server/src/Gallery_Plots_3D.hpp b/mcp/core/runtime/Gallery_Plots_3D.hpp similarity index 100% rename from web_server/src/Gallery_Plots_3D.hpp rename to mcp/core/runtime/Gallery_Plots_3D.hpp diff --git a/web_server/src/Plot.cpp b/mcp/core/runtime/Plot.cpp similarity index 100% rename from web_server/src/Plot.cpp rename to mcp/core/runtime/Plot.cpp diff --git a/web_server/src/Plot.hpp b/mcp/core/runtime/Plot.hpp similarity index 100% rename from web_server/src/Plot.hpp rename to mcp/core/runtime/Plot.hpp diff --git a/web_server/src/Renderable_Adapter.hpp b/mcp/core/runtime/Renderable_Adapter.hpp similarity index 94% rename from web_server/src/Renderable_Adapter.hpp rename to mcp/core/runtime/Renderable_Adapter.hpp index 1c88e54..169a0b2 100644 --- a/web_server/src/Renderable_Adapter.hpp +++ b/mcp/core/runtime/Renderable_Adapter.hpp @@ -24,7 +24,7 @@ public: [[nodiscard]] virtual std::string_view id() const noexcept = 0; [[nodiscard]] virtual nlohmann::json schema() const = 0; [[nodiscard]] virtual nlohmann::json state() const = 0; - [[nodiscard]] virtual nlohmann::json snapshot() const = 0; + [[nodiscard]] virtual nlohmann::json values() const = 0; [[nodiscard]] virtual nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) = 0; }; template @@ -34,7 +34,7 @@ public: [[nodiscard]] std::string_view id() const noexcept override; [[nodiscard]] nlohmann::json schema() const override; [[nodiscard]] nlohmann::json state() const override; - [[nodiscard]] nlohmann::json snapshot() const override; + [[nodiscard]] nlohmann::json values() const override; [[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override; private: std::string component_id; diff --git a/web_server/src/Renderable_Adapter.ipp b/mcp/core/runtime/Renderable_Adapter.ipp similarity index 67% rename from web_server/src/Renderable_Adapter.ipp rename to mcp/core/runtime/Renderable_Adapter.ipp index 6f26de7..89addc0 100644 --- a/web_server/src/Renderable_Adapter.ipp +++ b/mcp/core/runtime/Renderable_Adapter.ipp @@ -1,16 +1,13 @@ #pragma once -#include -#include +#include #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -114,138 +111,46 @@ struct Type_Descriptor -struct is_std_array : std::false_type {}; -template -struct is_std_array> : std::true_type {}; -template -inline constexpr bool is_std_array_v = is_std_array>::value; -template -struct is_pair : std::false_type {}; -template -struct is_pair> : std::true_type {}; -template -inline constexpr bool is_pair_v = is_pair>::value; -template -concept Json_Sequence = std::ranges::range && requires(T value, typename T::value_type item) { - value.clear(); - value.emplace_back(std::move(item)); -} && !std::same_as, std::string>; template -nlohmann::json encode_protocol_value(const Value& value); -template -void decode_protocol_value(Value& target, const nlohmann::json& input); -template -nlohmann::json encode_aggregate(const Value& value, std::index_sequence) { - nlohmann::json result = nlohmann::json::object(); - ((result[std::string(boost::pfr::get_name())] = - encode_protocol_value(boost::pfr::get < Index > (value))), ...); - return result; +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { + return mcp::protocol_editor(); } -template -void decode_aggregate(Value& target, const nlohmann::json& input, std::index_sequence) { - (decode_protocol_value(boost::pfr::get < Index > (target), - input.at(std::string(boost::pfr::get_name()))), ...); +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "color"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "color"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "point2"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "size"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "size"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "rect"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "range"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { + return "partition-grid"; } -template -nlohmann::json encode_protocol_value(const Value& value) { - using Type = std::remove_cvref_t; - if constexpr (std::same_as || std::integral || std::floating_point || std::same_as) { - return value; - } - else if constexpr (std::same_as) { - return std::string(1, value); - } - else if constexpr (std::is_enum_v) { - return magic_enum::enum_name(value); - } - else if constexpr (std::same_as) { - return {{"stops", encode_protocol_value(value.stops)}}; - } - else if constexpr (is_pair_v) { - return nlohmann::json::array({encode_protocol_value(value.first), encode_protocol_value(value.second)}); - } - else if constexpr (is_std_array_v || Json_Sequence) { - nlohmann::json result = nlohmann::json::array(); - for (const auto& item : value) result.push_back(encode_protocol_value(item)); - return result; - } - else if constexpr (std::is_aggregate_v) { - return encode_aggregate(value, std::make_index_sequence>{}); - } - else { - static_assert(std::is_aggregate_v, "Aethera protocol needs an explicit codec for this value type"); - } -} -template -void decode_protocol_value(Value& target, const nlohmann::json& input) { - using Type = std::remove_cvref_t; - if constexpr (std::same_as || std::integral || std::floating_point || std::same_as) { - target = input.template get(); - } - else if constexpr (std::same_as) { - const auto text = input.template get(); - if (text.size() != 1) throw std::invalid_argument("character value must contain exactly one character"); - target = text.front(); - } - else if constexpr (std::is_enum_v) { - const auto value = magic_enum::enum_cast(input.template get()); - if (!value) throw std::invalid_argument("unknown enum value"); - target = *value; - } - else if constexpr (std::same_as) { - decode_protocol_value(target.stops, input.at("stops")); - } - else if constexpr (is_pair_v) { - if (!input.is_array() || input.size() != 2) throw std::invalid_argument("pair value must be a two-item array"); - decode_protocol_value(target.first, input[0]); - decode_protocol_value(target.second, input[1]); - } - else if constexpr (is_std_array_v) { - if (!input.is_array() || input.size() != target.size()) throw std::invalid_argument("array has the wrong size"); - for (std::size_t index = 0; index < target.size(); ++index) decode_protocol_value(target[index], input[index]); - } - else if constexpr (Json_Sequence) { - if (!input.is_array()) throw std::invalid_argument("value must be an array"); - Type updated; - for (const auto& encoded : input) { - typename Type::value_type item{}; - decode_protocol_value(item, encoded); - updated.emplace_back(std::move(item)); - } - target = std::move(updated); - } - else if constexpr (std::is_aggregate_v) { - if (!input.is_object()) throw std::invalid_argument("value must be an object"); - decode_aggregate(target, input, std::make_index_sequence>{}); - } - else { - static_assert(std::is_aggregate_v, "Aethera protocol needs an explicit codec for this value type"); - } -} -template -constexpr std::string_view protocol_editor() { - using Type = std::remove_cvref_t; - if constexpr (std::same_as) return "boolean"; - if constexpr (std::integral) return "integer"; - if constexpr (std::floating_point) return "number"; - if constexpr (std::same_as || std::same_as) return "text"; - if constexpr (std::is_enum_v) return "select"; - if constexpr (std::same_as || std::same_as) return "color"; - if constexpr (std::same_as) return "point2"; - if constexpr (std::same_as || std::same_as) return "size"; - if constexpr (std::same_as) return "rect"; - if constexpr (std::same_as) return "range"; - if constexpr (std::same_as) return "partition-grid"; - if constexpr (std::same_as) return "pen"; - if constexpr (std::same_as) return "brush"; - if constexpr (std::same_as) return "font"; - if constexpr (std::same_as) return "color-map"; - if constexpr (std::same_as) return "vector3"; - if constexpr (std::same_as) return "matrix4"; - if constexpr (std::same_as>) return "marker-list"; - return "json"; +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "pen"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "brush"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "font"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "color-map"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "vector3"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag) noexcept { return "matrix4"; } +[[nodiscard]] constexpr std::string_view renderable_editor( + mcp::Type_Tag>) noexcept { + return "marker-list"; } + inline std::string_view protocol_field_label(std::string_view key) { static constexpr std::pair labels[] = { {"position", "位置"}, {"viewport", "视口尺寸"}, {"background", "背景颜色"}, @@ -314,7 +219,7 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std:: using Value = typename Field::value_type; const bool editable = field.template attribute().value == Renderable_Field_Role::prop; const auto field_label = protocol_field_label(field.key()); - std::string_view editor = protocol_editor(); + std::string_view editor = renderable_editor(mcp::Type_Tag{}); if constexpr (requires { Field::accessor_type::editor; }) { if (!Field::accessor_type::editor.view().empty()) editor = Field::accessor_type::editor.view(); } @@ -324,7 +229,7 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std:: {"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"}, {"technical_description", std::string(Field::accessor_type::description.view())} }; - if (editable) item["value"] = encode_protocol_value(field.accessor.read(adapter)); + if (editable) item["value"] = mcp::encode_protocol_value(field.accessor.read(adapter)); if constexpr (std::same_as) item["color_channel_scale"] = "normalized"; else if constexpr (std::same_as) item["color_channel_scale"] = "byte"; if (editable) { @@ -348,7 +253,7 @@ nlohmann::json adapter_values(const Adapter& adapter, Renderable_Field_Role role nlohmann::json result = nlohmann::json::object(); structive::type_descriptor().for_each_property([&](auto, const auto& field) { if (field.template attribute().value == role) - result[field.key()] = encode_protocol_value(field.accessor.read(adapter)); + result[field.key()] = mcp::encode_protocol_value(field.accessor.read(adapter)); }); return result; } @@ -364,14 +269,14 @@ nlohmann::json write_adapter_prop(Adapter& adapter, std::string_view key, const else { try { Value value{}; - decode_protocol_value(value, input); + mcp::decode_protocol_value(value, input); const auto status = adapter.runtime_write(key, typeid(Value), &value); if (status != structive::Runtime_Access_Result::ok) { result["error"] = "property write was rejected"; return; } result["success"] = true; - result["value"] = encode_protocol_value(field.accessor.read(adapter)); + result["value"] = mcp::encode_protocol_value(field.accessor.read(adapter)); } catch (const std::exception& error) { result["error"] = error.what(); @@ -401,7 +306,7 @@ nlohmann::json Renderable_Descriptor_Model::state() const { {"state", adapter_values(adapter, Renderable_Field_Role::state)}}; } template -nlohmann::json Renderable_Descriptor_Model::snapshot() const { +nlohmann::json Renderable_Descriptor_Model::values() const { return {{"component", component_id}, {"label", component_label}, {"kind", component_kind}, {"prop", adapter_values(adapter, Renderable_Field_Role::prop)}, diff --git a/web_server/src/Taskflow_Trace_Json.hpp b/mcp/core/runtime/Taskflow_Trace_Json.hpp similarity index 100% rename from web_server/src/Taskflow_Trace_Json.hpp rename to mcp/core/runtime/Taskflow_Trace_Json.hpp diff --git a/mcp/main.cmake b/mcp/main.cmake new file mode 100644 index 0000000..72fd301 --- /dev/null +++ b/mcp/main.cmake @@ -0,0 +1,92 @@ +include("${CMAKE_CURRENT_LIST_DIR}/../web_server/cmake/rely.cmake") +set(Aethera_MCP_dependencies + global::drogon + global::pfr) +rcl_add_dependency_action_targets(Aethera_MCP_env ${Aethera_MCP_dependencies}) +set_target_properties(Aethera_MCP_env PROPERTIES FOLDER Aethera_MCP) +library_get_missing_with_rely(Aethera_MCP_dependencies_missing + ${Aethera_MCP_dependencies}) +if (Aethera_MCP_dependencies_missing) + rcl_log_append(${CMAKE_CURRENT_LIST_LINE} + "[FATAL_ERROR] Aethera_MCP dependencies\n${Aethera_MCP_dependencies_missing}\n are not installed. Build Aethera_MCP_env first") + return() +endif () +rcl_load_dependency_environment(${Aethera_MCP_dependencies}) +find_package(Drogon CONFIG REQUIRED) +if (NOT TARGET Aethera_render_2D) + rcl_log_append(${CMAKE_CURRENT_LIST_LINE} + "[FATAL_ERROR] Aethera_MCP requires Aethera_render_2D") + return() +endif () +if (NOT TARGET Aethera_render_3D) + rcl_log_append(${CMAKE_CURRENT_LIST_LINE} + "[FATAL_ERROR] Aethera_MCP requires Aethera_render_3D") + return() +endif () +if (NOT TARGET structive::property_core) + set(Aethera_MCP_saved_build_testing "${BUILD_TESTING}") + set(BUILD_TESTING OFF) + set(STRUCTIVE_BUILD_EXAMPLES OFF) + set(STRUCTIVE_BUILD_TESTS OFF) + set(STRUCTIVE_INSTALL OFF) + add_subdirectory( + "${CMAKE_CURRENT_LIST_DIR}/../third_party/Structive" + "${CMAKE_CURRENT_BINARY_DIR}/third_party/Structive" + EXCLUDE_FROM_ALL) + set(BUILD_TESTING "${Aethera_MCP_saved_build_testing}") + unset(Aethera_MCP_saved_build_testing) +endif () +set(Aethera_MCP_core_dir "${CMAKE_CURRENT_LIST_DIR}/core") +append_glob_source(Aethera_MCP_core_sources "${Aethera_MCP_core_dir}") +add_library(Aethera_MCP_Core STATIC ${Aethera_MCP_core_sources}) +target_include_directories(Aethera_MCP_Core PUBLIC + "$" + "$" + "$") +target_compile_features(Aethera_MCP_Core PUBLIC cxx_std_23) +target_compile_definitions(Aethera_MCP_Core PUBLIC NOMINMAX) +target_link_libraries(Aethera_MCP_Core PUBLIC + Aethera_render_2D + Aethera_render_3D + structive::property_core) +add_executable(Aethera_MCP_Server + "${CMAKE_CURRENT_LIST_DIR}/server/main.cpp" + "${CMAKE_CURRENT_LIST_DIR}/server/Mcp_Server.cpp" + "${CMAKE_CURRENT_LIST_DIR}/server/Mcp_Server.hpp") +target_link_libraries(Aethera_MCP_Server PRIVATE + Aethera_MCP_Core + Drogon::Drogon + TBB::tbbmalloc_proxy) +if (MSVC) + target_compile_options(Aethera_MCP_Core PRIVATE /utf-8 /bigobj) + target_compile_options(Aethera_MCP_Server PRIVATE /utf-8 /bigobj) + target_link_options(Aethera_MCP_Server PRIVATE "/INCLUDE:__TBB_malloc_proxy") + add_custom_command(TARGET Aethera_MCP_Server POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" + "$" + "$" + COMMENT "Deploying the scalable allocator runtime for Aethera MCP" + VERBATIM) +endif () +if (Aethera_BUILD_TESTS) + add_executable(Aethera_MCP_Protocol_Type_Tests + "${CMAKE_CURRENT_LIST_DIR}/tests/Protocol_Type_Tests.cpp") + target_link_libraries(Aethera_MCP_Protocol_Type_Tests PRIVATE + Aethera_MCP_Core + GTest::gtest_main) + if (MSVC) + target_compile_options(Aethera_MCP_Protocol_Type_Tests PRIVATE + /utf-8 /bigobj) + endif () + add_test(NAME Aethera_MCP_Protocol_Type_Tests + COMMAND Aethera_MCP_Protocol_Type_Tests) + set_tests_properties(Aethera_MCP_Protocol_Type_Tests PROPERTIES + LABELS "Aethera_MCP") + add_custom_target(Aethera_MCP_check + COMMAND "${CMAKE_CTEST_COMMAND}" + --test-dir "${CMAKE_BINARY_DIR}" + -C "$" -L "^Aethera_MCP$" --output-on-failure + DEPENDS Aethera_MCP_Protocol_Type_Tests + USES_TERMINAL) +endif () diff --git a/mcp/server/Mcp_Server.cpp b/mcp/server/Mcp_Server.cpp new file mode 100644 index 0000000..258592b --- /dev/null +++ b/mcp/server/Mcp_Server.cpp @@ -0,0 +1,147 @@ +#include "Mcp_Server.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::mcp { +namespace { + +constexpr std::string_view protocol_version{"2026-07-28"}; + +[[nodiscard]] drogon::HttpResponsePtr json_response( + nlohmann::json value, + drogon::HttpStatusCode status = drogon::k200OK) { + auto response = drogon::HttpResponse::newHttpResponse(); + response->setContentTypeCode(drogon::CT_APPLICATION_JSON); + response->setStatusCode(status); + response->addHeader("MCP-Protocol-Version", std::string{protocol_version}); + response->setBody(value.dump()); + return response; +} + +[[nodiscard]] nlohmann::json rpc_error( + const nlohmann::json& id, int code, std::string message) { + return {{"jsonrpc", "2.0"}, {"id", id}, + {"error", {{"code", code}, {"message", std::move(message)}}}}; +} + +[[nodiscard]] bool accepted_origin(const drogon::HttpRequestPtr& request) { + const auto origin = request->getHeader("Origin"); + if (origin.empty()) return true; + return origin == "null" || origin.starts_with("http://127.0.0.1") || + origin.starts_with("http://localhost") || + origin.starts_with("https://127.0.0.1") || + origin.starts_with("https://localhost"); +} + +[[nodiscard]] nlohmann::json tool_result(const Tool_Call_Output& output) { + const bool failed = output.result != Tool_Call_Result::ok; + const auto text = failed ? output.message : output.content.dump(); + nlohmann::json result{ + {"content", nlohmann::json::array({{{"type", "text"}, {"text", text}}})}, + {"isError", failed}}; + if (!output.content.is_null() && !output.content.empty()) + result["structuredContent"] = output.content; + return result; +} + +[[nodiscard]] nlohmann::json dispatch_request( + Control_Service& control, const nlohmann::json& request) { + const auto id = request.value("id", nlohmann::json{}); + const auto method = request.value("method", std::string{}); + if (method == "server/discover" || method == "initialize") { + return {{"jsonrpc", "2.0"}, {"id", id}, + {"result", { + {"protocolVersion", protocol_version}, + {"capabilities", {{"tools", nlohmann::json::object()}}}, + {"serverInfo", {{"name", "aethera"}, {"version", "1.0.0"}}}}}}; + } + if (method == "ping") + return {{"jsonrpc", "2.0"}, {"id", id}, + {"result", nlohmann::json::object()}}; + if (method == "tools/list") + return {{"jsonrpc", "2.0"}, {"id", id}, + {"result", {{"tools", control.tool_catalog()}, + {"ttlMs", 1000}, {"cacheScope", "server"}}}}; + if (method == "tools/call") { + if (!request.contains("params") || !request["params"].is_object()) + return rpc_error(id, -32602, "tools/call requires params"); + const auto& parameters = request["params"]; + const auto name = parameters.value("name", std::string{}); + if (name.empty()) return rpc_error(id, -32602, "tool name is required"); + const auto arguments = parameters.value( + "arguments", nlohmann::json::object()); + const auto output = control.call_tool(name, arguments); + if (output.result == Tool_Call_Result::unknown_tool) + return rpc_error(id, -32602, output.message); + return {{"jsonrpc", "2.0"}, {"id", id}, + {"result", tool_result(output)}}; + } + return rpc_error(id, -32601, "method not found"); +} + +} + +int run_mcp_server(std::uint16_t port) { + auto control = Control_Service::create(); + auto& app = drogon::app(); + app.registerHandler( + "/mcp", + [control](const drogon::HttpRequestPtr& request, + std::function&& callback) { + if (!accepted_origin(request)) { + callback(json_response( + rpc_error(nullptr, -32000, "origin is not allowed"), + drogon::k403Forbidden)); + return; + } + if (request->method() == drogon::Get) { + callback(json_response( + rpc_error(nullptr, -32600, "SSE stream is not provided"), + drogon::k405MethodNotAllowed)); + return; + } + nlohmann::json message; + try { + message = nlohmann::json::parse(request->body()); + } catch (const nlohmann::json::exception&) { + callback(json_response( + rpc_error(nullptr, -32700, "invalid JSON"), + drogon::k400BadRequest)); + return; + } + if (!message.is_object() || message.value("jsonrpc", "") != "2.0" || + !message.contains("method")) { + callback(json_response( + rpc_error(message.value("id", nlohmann::json{}), + -32600, "invalid JSON-RPC request"), + drogon::k400BadRequest)); + return; + } + if (!message.contains("id")) { + auto response = drogon::HttpResponse::newHttpResponse(); + response->setStatusCode(drogon::k202Accepted); + callback(std::move(response)); + return; + } + try { + callback(json_response(dispatch_request(*control, message))); + } catch (const std::exception& failure) { + callback(json_response( + rpc_error(message["id"], -32603, failure.what()), + drogon::k500InternalServerError)); + } + }, + {drogon::Get, drogon::Post}); + app.addListener("127.0.0.1", port) + .setThreadNum(std::min(8U, std::max(2U, std::thread::hardware_concurrency()))) + .setIdleConnectionTimeout(90) + .run(); + return 0; +} + +} diff --git a/mcp/server/Mcp_Server.hpp b/mcp/server/Mcp_Server.hpp new file mode 100644 index 0000000..dc6b506 --- /dev/null +++ b/mcp/server/Mcp_Server.hpp @@ -0,0 +1,8 @@ +#pragma once +#include + +namespace aethera::mcp { + +int run_mcp_server(std::uint16_t port); + +} diff --git a/mcp/server/main.cpp b/mcp/server/main.cpp new file mode 100644 index 0000000..dde5691 --- /dev/null +++ b/mcp/server/main.cpp @@ -0,0 +1,32 @@ +#include "Mcp_Server.hpp" +#include +#include +#include +#include +#include + +namespace { + +[[nodiscard]] std::uint16_t parse_port(int argc, char** argv) { + constexpr std::uint16_t default_port{8850}; + if (argc == 1) return default_port; + if (argc != 3 || std::string_view{argv[1]} != "--port") return 0; + unsigned value{}; + const std::string_view text{argv[2]}; + const auto [end, error] = std::from_chars( + text.data(), text.data() + text.size(), value); + if (error != std::errc{} || end != text.data() + text.size() || + value == 0 || value > 65535) + return 0; + return static_cast(value); +} + +} + +int main(int argc, char** argv) { + const auto port = parse_port(argc, argv); + if (port == 0) return 2; + aethera::initialize_runtime({}); + std::cout << "Aethera MCP http://127.0.0.1:" << port << "/mcp\n"; + return aethera::mcp::run_mcp_server(port); +} diff --git a/mcp/tests/Protocol_Type_Tests.cpp b/mcp/tests/Protocol_Type_Tests.cpp new file mode 100644 index 0000000..a209fc2 --- /dev/null +++ b/mcp/tests/Protocol_Type_Tests.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +namespace aethera::mcp::tests { + +enum struct Sample_Mode : std::uint8_t { + first, + second +}; + +struct Nested_Value { + double gain{}; /* 测试嵌套数值。 */ + Sample_Mode mode{Sample_Mode::first}; /* 测试枚举描述。 */ + bool operator==(const Nested_Value&) const = default; +}; + +struct Request_Value { + std::string plot; /* 测试字段名反射。 */ + std::vector values; /* 测试递归容器。 */ + bool operator==(const Request_Value&) const = default; +}; + +struct Semantic_Value { + int value{}; /* 自定义类型值。 */ + bool operator==(const Semantic_Value&) const = default; +}; + +struct Semantic_Value_Type : Aggregate_Type { + [[nodiscard]] static constexpr std::string_view editor() noexcept { + return "semantic-value"; + } +}; + +/* 自定义类型把精确重载直接放在 struct 定义之后;PFR 通用分派无需修改。 */ +[[nodiscard]] auto match_protocol_type( + Type_Tag) -> Semantic_Value_Type; + +TEST(Protocol_Type, Pfr_Field_Names_Drive_Recursive_Schema) { + const auto schema = describe_protocol_type(); + ASSERT_EQ(schema.at("type"), "object"); + ASSERT_TRUE(schema.at("properties").contains("plot")); + ASSERT_TRUE(schema.at("properties").contains("values")); + const auto& nested = schema.at("properties").at("values").at("items"); + EXPECT_TRUE(nested.at("properties").contains("gain")); + EXPECT_EQ(nested.at("properties").at("mode").at("enum").size(), 2); +} + +TEST(Protocol_Type, Pfr_Codec_Round_Trips_Nested_Values) { + const Request_Value input{"plot-3d", {{1.5, Sample_Mode::second}}}; + const auto encoded = encode_protocol_value(input); + Request_Value decoded{}; + decode_protocol_value(decoded, encoded); + EXPECT_EQ(decoded, input); +} + +TEST(Protocol_Type, Exact_Matcher_Overload_Wins_Without_If_Dispatch) { + EXPECT_EQ(protocol_editor(), "semantic-value"); + EXPECT_EQ(encode_protocol_value(Semantic_Value{7}).at("value"), 7); +} + +TEST(Protocol_Type, Non_Aggregate_Render_Type_Has_Explicit_Matcher) { + render_2d::Color_Map color_map; + color_map.stops = {Color::red_color(), Color::green_color()}; + const auto encoded = encode_protocol_value(color_map); + render_2d::Color_Map decoded; + decode_protocol_value(decoded, encoded); + EXPECT_EQ(decoded, color_map); +} + +} diff --git a/web_server/main.cmake b/web_server/main.cmake index bbfa3e6..791d3cc 100644 --- a/web_server/main.cmake +++ b/web_server/main.cmake @@ -139,6 +139,7 @@ target_include_directories(Aethera_Web_Core PUBLIC target_compile_features(Aethera_Web_Core PUBLIC cxx_std_20) target_compile_definitions(Aethera_Web_Core PUBLIC NOMINMAX) target_link_libraries(Aethera_Web_Core PUBLIC + Aethera_MCP_Core Aethera_render_2D Aethera_render_3D Drogon::Drogon diff --git a/web_server/src/Gallery_Video_Stream.hpp b/web_server/src/Gallery_Video_Stream.hpp index 137be5a..15c2e6d 100644 --- a/web_server/src/Gallery_Video_Stream.hpp +++ b/web_server/src/Gallery_Video_Stream.hpp @@ -1,5 +1,5 @@ #pragma once -#include "Plot.hpp" +#include #include "frame_sampling/ffmpeg/FFmpeg_Frame_Transport.hpp" #include "frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.hpp" #include diff --git a/web_server/src/Graph_WebSocket.hpp b/web_server/src/Graph_WebSocket.hpp index 5bb8b3b..89e07b5 100644 --- a/web_server/src/Graph_WebSocket.hpp +++ b/web_server/src/Graph_WebSocket.hpp @@ -1,5 +1,5 @@ #pragma once -#include "Plot.hpp" +#include #include #include #include diff --git a/web_server/src/Web_Server.cpp b/web_server/src/Web_Server.cpp index 8266484..a02afd9 100644 --- a/web_server/src/Web_Server.cpp +++ b/web_server/src/Web_Server.cpp @@ -2,7 +2,8 @@ #include "Gallery_Video_Stream.hpp" #include "Gallery_WebSocket.hpp" #include "Graph_WebSocket.hpp" -#include "Gallery_Plots.hpp" +#include +#include #include #include #include @@ -17,7 +18,6 @@ namespace aethera::web { namespace { -using Plot_Map = std::unordered_map>; using Gallery_Stream_Map = std::unordered_map>; @@ -34,70 +34,6 @@ drogon::HttpResponsePtr error_response(drogon::HttpStatusCode status, std::strin return response; } -std::shared_ptr find_plot(const Plot_Map& plots, std::string_view id) { - const auto found = plots.find(std::string(id)); - return found == plots.end() ? nullptr : found->second; -} - -nlohmann::json taskflow_runtime_json() { - const auto state = aethera::task_runtime_state(); - nlohmann::json workers = nlohmann::json::array(); - for (const auto& worker : state.workers) - workers.push_back({ - {"id", worker.id}, {"task_count", worker.task_count}, - {"current_queue_size", worker.current_queue_size}, - {"current_queue_capacity", worker.current_queue_capacity}, - {"peak_queue_size", worker.peak_observed_queue_size}, - {"max_queue_capacity", worker.max_observed_queue_capacity}, - {"active_task", {{"native_id", std::to_string(worker.active_task_hash)}, - {"type", worker.active_task_type}, - {"time_ns", worker.active_task_time_ns}}}, - {"task_time_ns", worker.task_time_ns}, - {"busy_time_ns", worker.busy_time_ns}, - {"cpu_time_ns", worker.cpu_time_ns}, - {"non_cpu_time_ns", worker.non_cpu_time_ns}, - {"idle_time_ns", worker.idle_time_ns}, - {"min_task_time_ns", worker.min_task_time_ns}, - {"max_task_time_ns", worker.max_task_time_ns}, - {"utilization", worker.utilization}, - {"cpu_utilization", worker.cpu_utilization}}); - nlohmann::json task_types = nlohmann::json::array(); - for (const auto& type : state.task_types) - task_types.push_back({ - {"name", type.name}, {"count", type.count}, - {"total_time_ns", type.total_time_ns}, - {"min_time_ns", type.min_time_ns}, - {"max_time_ns", type.max_time_ns}}); - return { - {"protocol", "aethera.taskflow.runtime"}, {"version", 1}, - {"worker_count", state.worker_count}, - {"active_topologies", state.active_topology_count}, - {"active_taskflows", state.active_taskflow_count}, - {"peak_active_taskflows", state.peak_active_taskflow_count}, - {"completed_taskflows", state.completed_taskflow_count}, - {"failed_taskflows", state.failed_taskflow_count}, - {"active_tasks", state.active_task_count}, - {"peak_active_tasks", state.peak_active_task_count}, - {"active_workers", state.active_worker_count}, - {"peak_active_workers", state.peak_active_worker_count}, - {"observed_tasks", state.observed_task_count}, - {"named_tasks", state.named_task_count}, - {"peak_worker_queue_size", state.peak_observed_worker_queue_size}, - {"max_worker_queue_capacity", state.max_observed_worker_queue_capacity}, - {"longest_task", {{"native_id", std::to_string(state.longest_task_hash)}, - {"name", state.longest_task_name}, - {"type", state.longest_task_type}, - {"time_ns", state.longest_task_time_ns}}}, - {"total_task_time_ns", state.total_task_time_ns}, - {"worker_busy_time_ns", state.worker_busy_time_ns}, - {"worker_cpu_time_ns", state.worker_cpu_time_ns}, - {"observed_wall_time_ns", state.observed_wall_time_ns}, - {"worker_utilization", state.worker_utilization}, - {"worker_cpu_utilization", state.worker_cpu_utilization}, - {"task_types", std::move(task_types)}, - {"workers", std::move(workers)}}; -} - bool taskflow_graph_contains_gallery_media(const nlohmann::json& graph) { if (!graph.is_object() || !graph.contains("nodes") || !graph["nodes"].is_array()) @@ -177,17 +113,14 @@ nlohmann::json merge_gallery_media_trace(nlohmann::json plot_trace, } int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) { - auto plots = std::make_shared(); - plots->reserve(gallery_plot_definitions().size()); - for (const auto& definition : gallery_plot_definitions()) - plots->emplace(definition.id, definition.create()); + auto control = mcp::Control_Service::create(); std::vector gallery_2d; std::vector gallery_3d; gallery_2d.reserve(8); gallery_3d.reserve(16); for (const auto& definition : gallery_plot_definitions()) { - auto plot = plots->at(std::string{definition.id}); + auto plot = control->find_plot(definition.id); (definition.dimension == Plot_Dimension::three_d ? gallery_3d : gallery_2d) .push_back({std::string{definition.id}, std::move(plot)}); @@ -236,7 +169,9 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) }; add_media_groups("2d", std::move(gallery_2d)); add_media_groups("3d", std::move(gallery_3d)); - auto resolve_plot = [plots](std::string_view id) { return find_plot(*plots, id); }; + auto resolve_plot = [control](std::string_view id) { + return control->find_plot(id); + }; auto websocket = std::make_shared(resolve_plot); auto gallery_websocket = std::make_shared( [gallery_streams](std::string_view id) { @@ -245,7 +180,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) }); auto& app = drogon::app(); - app.registerHandler("/plot", [plots, plot_media](const drogon::HttpRequestPtr&, + app.registerHandler("/plot", [control, plot_media](const drogon::HttpRequestPtr&, std::function&& callback) { nlohmann::json result = nlohmann::json::array(); for (const auto& definition : gallery_plot_definitions()) { @@ -278,11 +213,11 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) callback(std::move(response)); }, {drogon::Get}); - app.registerHandler("/plot/{1}/diagnostics", [plots]( + app.registerHandler("/plot/{1}/diagnostics", [control]( const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id) { - auto plot = find_plot(*plots, plot_id); + auto plot = control->find_plot(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; @@ -295,24 +230,25 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) callback(json_response(plot->diagnostics())); }, {drogon::Get, drogon::Delete}); - app.registerHandler("/taskflow/diagnostics", []( + app.registerHandler("/taskflow/diagnostics", [control]( const drogon::HttpRequestPtr&, std::function&& callback) { - callback(json_response(taskflow_runtime_json())); + const auto output = control->call_tool("aethera_task_runtime", nlohmann::json::object()); + callback(json_response(output.content)); }, {drogon::Get}); - app.registerHandler("/plot/{1}/taskflow", [plots, plot_media_source]( + app.registerHandler("/plot/{1}/taskflow", [control, plot_media_source]( const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id) { - auto plot = find_plot(*plots, plot_id); + auto plot = control->find_plot(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; } const auto source_found = plot_media_source->find(plot_id); auto media_plot = source_found == plot_media_source->end() - ? plot : find_plot(*plots, source_found->second); + ? plot : control->find_plot(source_found->second); if (!media_plot) media_plot = plot; try { if (request->method() == drogon::Post) { @@ -380,11 +316,11 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) callback(json_response(found->second->diagnostics())); }, {drogon::Get}); - app.registerHandler("/plot/{1}/schema", [plots]( + app.registerHandler("/plot/{1}/schema", [control]( const drogon::HttpRequestPtr&, std::function&& callback, std::string plot_id) { - auto plot = find_plot(*plots, plot_id); + auto plot = control->find_plot(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; @@ -396,13 +332,13 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) } }, {drogon::Get}); - app.registerHandler("/plot/{1}/component/{2}/prop/{3}", [plots]( + app.registerHandler("/plot/{1}/component/{2}/prop/{3}", [control]( const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id, std::string component, std::string key) { - auto plot = find_plot(*plots, plot_id); + auto plot = control->find_plot(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; @@ -423,12 +359,12 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) } }, {drogon::Put}); - app.registerHandler("/plot/{1}/component/{2}/state", [plots]( + app.registerHandler("/plot/{1}/component/{2}/state", [control]( const drogon::HttpRequestPtr&, std::function&& callback, std::string plot_id, std::string component) { - auto plot = find_plot(*plots, plot_id); + auto plot = control->find_plot(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; @@ -445,11 +381,11 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) } }, {drogon::Get}); - app.registerHandler("/plot/{1}/data/generate", [plots]( + app.registerHandler("/plot/{1}/data/generate", [control]( const drogon::HttpRequestPtr& request, std::function&& callback, std::string plot_id) { - auto plot = find_plot(*plots, plot_id); + auto plot = control->find_plot(plot_id); if (!plot) { callback(error_response(drogon::k404NotFound, "unknown plot")); return; diff --git a/web_server/src/detail/Gallery_Frame_Atlas.hpp b/web_server/src/detail/Gallery_Frame_Atlas.hpp index 91dbfc8..0677341 100644 --- a/web_server/src/detail/Gallery_Frame_Atlas.hpp +++ b/web_server/src/detail/Gallery_Frame_Atlas.hpp @@ -1,5 +1,5 @@ #pragma once -#include "../Plot.hpp" +#include #include #include #include diff --git a/web_server/src/frame_sampling/Frame_Sampler.hpp b/web_server/src/frame_sampling/Frame_Sampler.hpp index 84c7c89..2fac74e 100644 --- a/web_server/src/frame_sampling/Frame_Sampler.hpp +++ b/web_server/src/frame_sampling/Frame_Sampler.hpp @@ -1,5 +1,5 @@ #pragma once -#include "../Plot.hpp" +#include #include "../detail/Gallery_Frame_Atlas.hpp" #include #include diff --git a/web_server/tests/Gallery_Plots_Tests.cpp b/web_server/tests/Gallery_Plots_Tests.cpp index 4478ae2..6c2bd6f 100644 --- a/web_server/tests/Gallery_Plots_Tests.cpp +++ b/web_server/tests/Gallery_Plots_Tests.cpp @@ -1,4 +1,4 @@ -#include "../src/Gallery_Plots.hpp" +#include #include #include