From f1d748a10486df457ef4b13ac2c8b2fd332432fc Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Wed, 8 Apr 2026 09:00:50 -0500 Subject: [PATCH 01/45] Refactor the UI test engine to make way for MCP. --- source/MRViewer/MRUITestEngineControl.cpp | 314 +++++++++++++++ source/MRViewer/MRUITestEngineControl.h | 101 +++++ source/mrviewerpy/MRPythonUiInteraction.cpp | 418 ++++---------------- 3 files changed, 482 insertions(+), 351 deletions(-) create mode 100644 source/MRViewer/MRUITestEngineControl.cpp create mode 100644 source/MRViewer/MRUITestEngineControl.h diff --git a/source/MRViewer/MRUITestEngineControl.cpp b/source/MRViewer/MRUITestEngineControl.cpp new file mode 100644 index 000000000000..4b5afbdd2824 --- /dev/null +++ b/source/MRViewer/MRUITestEngineControl.cpp @@ -0,0 +1,314 @@ +#include "MRUITestEngineControl.h" + +#include "MRMesh/MRMeshFwd.h" +#include "MRPch/MRFmt.h" +#include "MRViewer/MRUITestEngine.h" + +#include +#include +#include + +namespace MR::UI::TestEngine::Control +{ + +static std::string listKeys( const MR::UI::TestEngine::GroupEntry& group ) +{ + std::string ret; + bool first = true; + for ( const auto& elem : group.elems ) + { + if ( first ) + first = false; + else + ret += ", "; + ret += '`'; + ret += elem.first; + ret += '`'; + } + return ret; +} + +static const Expected findGroup( std::span path ) +{ + const TestEngine::GroupEntry* cur = &TestEngine::getRootEntry(); + for ( const auto& segment : path ) + { + auto iter = cur->elems.find( segment ); + if ( iter == cur->elems.end() ) + return unexpected( fmt::format( "No such entry: `{}`. Known entries are: {}.", segment, listKeys( *cur ) ) ); + auto ex = iter->second.getAs( segment ); + if (!ex) + return unexpected( ex.error() ); + cur = *ex; + } + return cur; +} + +std::string pathToString( const std::vector& path ) +{ + std::string pathString; + for ( const auto & s : path ) + { + if ( !pathString.empty() ) + pathString += '/'; + pathString += s; + } + return pathString; +} + +Expected> listEntries( const std::vector& path ) +{ + auto groupEx = findGroup( path ); + if ( !groupEx ) + return unexpected( groupEx.error() ); + const auto& group = **groupEx; + + std::vector ret; + ret.reserve( group.elems.size() ); + + for ( const auto& elem : group.elems ) + { + ret.push_back( { + .name = elem.first, + .type = std::visit( MR::overloaded{ + []( const TestEngine::ButtonEntry& ) { return EntryType::button; }, + []( const TestEngine::ValueEntry& e ) + { + return std::visit( MR::overloaded{ + []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueInt; }, + []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueUint; }, + []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueReal; }, + []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueString; }, + }, e.value ); + }, + []( const TestEngine::GroupEntry& ) { return EntryType::group; }, + }, elem.second.value ), + } ); + } + return ret; +} + +Expected pressButton( const std::vector& path ) +{ + if ( path.empty() ) + return unexpected( "pressButton: Empty path not allowed here." ); + + auto groupEx = findGroup( { path.data(), path.size() - 1 } ); + if ( !groupEx ) + return unexpected( groupEx.error() ); + const auto& group = **groupEx; + + auto iter = group.elems.find( path.back() ); + if ( iter == group.elems.end() ) + unexpected( fmt::format( "pressButton {}: no such entry: `{}`. Known entries are: {}.", pathToString( path ), path.back(), listKeys( group ) ) ); + + auto buttonEx = iter->second.getAs( path.back() ); + if ( !buttonEx ) + return unexpected( buttonEx.error() ); + + ( *buttonEx )->simulateClick = true; + + return {}; +} + +template +Expected> readValue( const std::vector& path ) +{ + if ( path.empty() ) + return unexpected( "readValue: Empty path not allowed here." ); + + auto groupEx = findGroup( { path.data(), path.size() - 1 } ); + if ( !groupEx ) + return unexpected( groupEx.error() ); + const auto& group = **groupEx; + + auto iter = group.elems.find( path.back() ); + if ( iter == group.elems.end() ) + return unexpected( fmt::format( "No such entry: `{}`. Known entries are: {}.", path.back(), listKeys( group ) ) ); + + auto entryEx = iter->second.getAs( path.back() ); + if ( !entryEx ) + return unexpected( entryEx.error() ); + const auto& entry = **entryEx; + + if constexpr ( std::is_same_v ) + { + if ( auto val = std::get_if>( &entry.value ) ) + { + Value ret; + ret.value = val->value; + ret.allowedValues = val->allowedValues; + return ret; + } + + return unexpected( "This isn't a string." ); + } + else + { + // Try to read with the wrong signedness first. + if constexpr ( std::is_same_v ) + { + if ( auto val = std::get_if>( &entry.value ) ) + { + // Allow if the value is not too large. + // We don't check if the max bound is too large, because it be too large by default if not specified. + + if ( val->value > std::uint64_t( std::numeric_limits::max() ) ) + return unexpected( "Attempt to read an uint64_t value as an int64_t, but the value is too large to fit into the target type. Read as uint64_t instead." ); + + Value ret; + ret.value = std::int64_t( val->value ); + ret.min = std::int64_t( std::min( val->min, std::uint64_t( std::numeric_limits::max() ) ) ); + ret.max = std::int64_t( std::min( val->max, std::uint64_t( std::numeric_limits::max() ) ) ); + return ret; + } + } + else if constexpr ( std::is_same_v ) + { + if ( auto val = std::get_if>( &entry.value ) ) + { + // Allow if the value is nonnegative, and the min bound is also nonnegative. + + if ( val->value < 0 || val->min < 0 ) + return unexpected( "Attempt to read an int64_t value as a uint64_t, but it is or can be negative. Read as int64_t instead." ); + + Value ret; + ret.value = std::uint64_t( val->value ); + ret.min = std::uint64_t( val->min ); + ret.max = std::uint64_t( val->max ); + return ret; + } + } + + if ( auto val = std::get_if>( &entry.value ) ) + { + Value ret; + ret.value = val->value; + ret.min = val->min; + ret.max = val->max; + return ret; + } + + return unexpected( std::is_floating_point_v + ? "This isn't a floating-point value." + : "This isn't an integer." + ); + } +} + +template Expected> readValue( const std::vector& path ); +template Expected> readValue( const std::vector& path ); +template Expected> readValue( const std::vector& path ); +template Expected> readValue( const std::vector& path ); + + +template +Expected writeValue( const std::vector& path, T value ) +{ + if ( path.empty() ) + return unexpected( "writeValue: Empty path not allowed here." ); + + auto groupEx = findGroup( { path.data(), path.size() - 1 } ); + if ( !groupEx ) + return unexpected( groupEx.error() ); + const auto& group = **groupEx; + + auto iter = group.elems.find( path.back() ); + if ( iter == group.elems.end() ) + return unexpected( fmt::format( "writeValue {}: no such entry: `{}`. Known entries are: {}.", pathToString( path ), path.back(), listKeys( group ) ) ); + + auto entryEx = iter->second.getAs( path.back() ); + if ( !entryEx ) + return unexpected( entryEx.error() ); + const auto& entry = **entryEx; + + auto writeValueOfCorrectType = [&entry, &path]( auto fixedValue ) -> Expected + { + using U = decltype( fixedValue ); + auto &target = std::get>( entry.value ); + + // Validate the value. + if constexpr ( std::is_same_v ) + { + if ( target.allowedValues && std::find( target.allowedValues->begin(), target.allowedValues->end(), fixedValue ) == target.allowedValues->end() ) + { + std::string allowedValuesStr; + bool first = true; + for ( const auto& allowedValue : *target.allowedValues ) + { + if ( !std::exchange( first, false ) ) + allowedValuesStr += ", "; + + allowedValuesStr += '`'; + allowedValuesStr += allowedValue; + allowedValuesStr += '`'; + } + + return unexpected( fmt::format( "writeValue {}: string `{}` is not allowed here. Allowed values: {}.", pathToString( path ), fixedValue, allowedValuesStr ) ); + } + } + else + { + if ( fixedValue < target.min ) + return unexpected( fmt::format( "writeValue {}: the specified value {} is less than the min bound {}.", pathToString( path ), fixedValue, target.min ) ); + if ( fixedValue > target.max ) + return unexpected( fmt::format( "writeValue {}: the specified value {} is more than the max bound {}.", pathToString( path ), fixedValue, target.max ) ); + } + + std::get>( entry.value ).simulatedValue = std::move( fixedValue ); + + return {}; + }; + + if constexpr ( std::is_same_v ) + { + if ( std::holds_alternative>( entry.value ) ) + return writeValueOfCorrectType( std::move( value ) ); + else + return unexpected( fmt::format( "writeValue: `{}` is a number, but received a string.", pathToString( path ) ) ); + } + else if constexpr ( std::is_same_v ) + { + return std::visit( MR::overloaded{ + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathToString( path ) ) ); }, + }, entry.value ); + } + else if constexpr ( std::is_same_v ) + { + return std::visit( MR::overloaded{ + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( double( value ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected + { + if ( value < 0 ) + return unexpected( fmt::format( "writeValue: `{}` is unsigned, but received a negative number.", pathToString( path ) ) ); + return writeValueOfCorrectType( std::uint64_t( value ) ); + }, + }, entry.value ); + } + else if constexpr ( std::is_same_v ) + { + return std::visit( MR::overloaded{ + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return unexpected( fmt::format( "writeValue: `{}` is a string, but received a number.", pathToString( path ) ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( double( value ) ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected { return writeValueOfCorrectType( value ); }, + [&]( const TestEngine::ValueEntry::Value& ) -> Expected + { + if ( value > std::uint64_t( std::numeric_limits::max() ) ) + return unexpected( fmt::format( "writeValue: `{}` is signed, but received an unsigned integer large enough to not be representable as `int64_t`.", pathToString( path ) ) ); + return writeValueOfCorrectType( std::int64_t( value ) ); + }, + }, entry.value ); + } +} + +template Expected writeValue( const std::vector& path, std::int64_t value ); +template Expected writeValue( const std::vector& path, std::uint64_t value ); +template Expected writeValue( const std::vector& path, double value ); +template Expected writeValue( const std::vector& path, std::string value ); + +} // namespace MR::UI::TestEngine::Control diff --git a/source/MRViewer/MRUITestEngineControl.h b/source/MRViewer/MRUITestEngineControl.h new file mode 100644 index 000000000000..37a131436224 --- /dev/null +++ b/source/MRViewer/MRUITestEngineControl.h @@ -0,0 +1,101 @@ +#pragma once + +#include "exports.h" +#include "MRMesh/MRExpected.h" + +#include +#include +#include +#include + +namespace MR::UI::TestEngine::Control +{ + +// Most changes in this file must be synced with: +// * Python: `MeshLib/source/mrviewerpy/MRPythonUiInteraction.cpp`. +// * MCP: `source/MRInspector/MRMcp.cpp`. + +enum class EntryType +{ + button, + group, + valueInt, + valueUint, + valueReal, + valueString, +}; + +[[nodiscard]] inline std::string_view toString( EntryType type ) +{ + const char* ret = nullptr; + switch ( type ) + { + case Control::EntryType::button: ret = "button"; break; + case Control::EntryType::valueInt: ret = "valueInt"; break; + case Control::EntryType::valueUint: ret = "valueUint"; break; + case Control::EntryType::valueReal: ret = "valueReal"; break; + case Control::EntryType::valueString: ret = "valueString"; break; + case Control::EntryType::group: ret = "group"; break; + } + assert( ret && "Unknown enum." ); + if ( !ret ) + ret = "??"; + return ret; +} + +struct TypedEntry +{ + std::string name; + EntryType type; +}; + +// Returns the elements of `path` combined into a single string. +[[nodiscard]] MRVIEWER_API std::string pathToString( const std::vector& path ); + +// Returns the contents of `path`, or an error if the path is wrong. +[[nodiscard]] MRVIEWER_API Expected> listEntries( const std::vector& path ); + +// Presses the button at this path, or returns an error. +MRVIEWER_API Expected pressButton( const std::vector& path ); + +// Read/write values: (drags, sliders, etc) + +template +struct Value +{ + T value = 0; + T min = 0; + T max = 0; +}; +template <> +struct Value +{ + std::string value; + + std::optional> allowedValues; +}; +using ValueInt = Value; +using ValueUint = Value; +using ValueReal = Value; +using ValueString = Value; + +// Returns the value at the `path`, or returns an error if the path or type is wrong. +template +MRVIEWER_API Expected> readValue( const std::vector& path ); + +extern template MRVIEWER_API Expected> readValue( const std::vector& path ); +extern template MRVIEWER_API Expected> readValue( const std::vector& path ); +extern template MRVIEWER_API Expected> readValue( const std::vector& path ); +extern template MRVIEWER_API Expected> readValue( const std::vector& path ); + +// Modifies the value at the `path`, or returns an error if the path, type or value are wrong. +template +MRVIEWER_API Expected writeValue( const std::vector& path, T value ); + +extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::int64_t value ); +extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::uint64_t value ); +extern template MRVIEWER_API Expected writeValue( const std::vector& path, double value ); +extern template MRVIEWER_API Expected writeValue( const std::vector& path, std::string value ); + + +} // namespace MR::UI::TestEngine::Control diff --git a/source/mrviewerpy/MRPythonUiInteraction.cpp b/source/mrviewerpy/MRPythonUiInteraction.cpp index 8b381ee0f1ac..a074dd13393c 100644 --- a/source/mrviewerpy/MRPythonUiInteraction.cpp +++ b/source/mrviewerpy/MRPythonUiInteraction.cpp @@ -1,6 +1,6 @@ #include "MRPython/MRPython.h" #include "MRViewer/MRPythonAppendCommand.h" -#include "MRViewer/MRUITestEngine.h" +#include "MRViewer/MRUITestEngineControl.h" #include "MRViewer/MRViewer.h" #include "MRPch/MRFmt.h" #include "MRPch/MRSpdlog.h" @@ -9,365 +9,82 @@ #include -namespace TestEngine = MR::UI::TestEngine; +namespace Control = MR::UI::TestEngine::Control; -namespace +MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiEntry, Control::TypedEntry ) +MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueInt, Control::ValueInt ) +MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueUint, Control::ValueUint ) +MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueReal, Control::ValueReal ) +MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueString, Control::ValueString ) + +MR_ADD_PYTHON_CUSTOM_DEF( mrviewerpy, UiEntry, [] ( pybind11::module_& m ) { - enum class EntryType - { - button, - group, - valueInt, - valueUint, - valueReal, - valueString, - other, - // Don't forget to add new values to `pybind11::enum_` below! - }; + // Not using `MR_ADD_PYTHON_VEC(..., TypedEntry)` here, I don't seem to need any of custom functions it provides. + + pybind11::enum_( m, "UiEntryType", "UI entry type enum." ) + .value( "button", Control::EntryType::button ) + .value( "group", Control::EntryType::group ) + .value( "valueInt", Control::EntryType::valueInt ) + .value( "valueUint", Control::EntryType::valueUint ) + .value( "valueReal", Control::EntryType::valueReal ) + .value( "valueString", Control::EntryType::valueString ) + ; - struct TypedEntry - { - std::string name; - EntryType type; - }; + MR_PYTHON_CUSTOM_CLASS( UiValueInt ).def_readonly( "value", &Control::ValueInt::value ).def_readonly( "min", &Control::ValueInt::min ).def_readonly( "max", &Control::ValueInt::max ); + MR_PYTHON_CUSTOM_CLASS( UiValueUint ).def_readonly( "value", &Control::ValueUint::value ).def_readonly( "min", &Control::ValueUint::min ).def_readonly( "max", &Control::ValueUint::max ); + MR_PYTHON_CUSTOM_CLASS( UiValueReal ).def_readonly( "value", &Control::ValueReal::value ).def_readonly( "min", &Control::ValueReal::min ).def_readonly( "max", &Control::ValueReal::max ); + MR_PYTHON_CUSTOM_CLASS( UiValueString ).def_readonly( "value", &Control::ValueString::value ).def_readonly( "allowed", &Control::ValueString::allowedValues ); - std::string listKeys( const MR::UI::TestEngine::GroupEntry& group ) - { - std::string ret; - bool first = true; - for ( const auto& elem : group.elems ) - { - if ( first ) - first = false; - else - ret += ", "; - ret += '`'; - ret += elem.first; - ret += '`'; - } - return ret; - } - - const TestEngine::GroupEntry& findGroup( std::span path ) - { - const TestEngine::GroupEntry* cur = &TestEngine::getRootEntry(); - for ( const auto& segment : path ) + MR_PYTHON_CUSTOM_CLASS( UiEntry ) + .def_readonly( "name", &Control::TypedEntry::name ) + .def_readonly( "type", &Control::TypedEntry::type ) + .def("__repr__", []( const Control::TypedEntry& e ) { - auto iter = cur->elems.find( segment ); - if ( iter == cur->elems.end() ) - throw std::runtime_error( fmt::format( "No such entry: `{}`. Known entries are: {}.", segment, listKeys( *cur ) ) ); - cur = MR::expectedValueOrThrow( iter->second.getAs( segment ) ); - } - return *cur; - } + return fmt::format( "", e.name, toString( e.type ) ); + } ) + ; +} ) - // Not using `MR_ADD_PYTHON_VEC` here, I don't seem to need any of custom functions it provides. - std::vector listEntries( const std::vector& path ) +MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiListEntries, + []( const std::vector& path ) { - std::vector ret; - MR::CommandLoop::runCommandFromGUIThread( [&] - { - const auto& group = findGroup( path ); - ret.reserve( group.elems.size() ); - for ( const auto& elem : group.elems ) - { - ret.push_back( { - .name = elem.first, - .type = std::visit( MR::overloaded{ - []( const TestEngine::ButtonEntry& ) { return EntryType::button; }, - []( const TestEngine::ValueEntry& e ) - { - return std::visit( MR::overloaded{ - []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueInt; }, - []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueUint; }, - []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueReal; }, - []( const TestEngine::ValueEntry::Value& ){ return EntryType::valueString; }, - }, e.value ); - }, - []( const TestEngine::GroupEntry& ) { return EntryType::group; }, - []( const auto& ) { return EntryType::other; }, - }, elem.second.value ), - } ); - } - } ); + std::vector ret; + MR::CommandLoop::runCommandFromGUIThread( [&]{ ret = MR::expectedValueOrThrow( MR::UI::TestEngine::Control::listEntries( path ) ); } ); return ret; - } - - static std::string stringVectorToString( const std::vector& path ) - { - std::string pathString; - for ( const auto & s : path ) - { - if ( !pathString.empty() ) - pathString += '/'; - pathString += s; - } - return pathString; - } - - void pressButton( const std::vector& path ) + }, + "List existing UI entries at the specified path.\n" + "Pass an empty list to see top-level groups.\n" + "Add group name to the end of the vector to see its contents.\n" + "When you find the button you need, pass it to `uiPressButton()`." +) +MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiPressButton, + []( const std::vector& path ) { - if ( path.empty() ) - throw std::runtime_error( "pressButton: empty path not allowed here." ); - const std::string pathString = stringVectorToString( path ); MR::CommandLoop::runCommandFromGUIThread( [&] { - spdlog::info( "pressButton {}: frame {}", pathString, MR::getViewerInstance().getTotalFrames() ); - - auto& group = findGroup( { path.data(), path.size() - 1 } ); - auto iter = group.elems.find( path.back() ); - if ( iter == group.elems.end() ) - throw std::runtime_error( fmt::format( "pressButton {}: no such entry: `{}`. Known entries are: {}.", pathString, path.back(), listKeys( group ) ) ); - MR::expectedValueOrThrow( iter->second.getAs( path.back() ) )->simulateClick = true; + spdlog::info( "pressButton {}: frame {}", MR::UI::TestEngine::Control::pathToString( path ), MR::getViewerInstance().getTotalFrames() ); + MR::expectedValueOrThrow( MR::UI::TestEngine::Control::pressButton( path ) ); } ); for ( int i = 0; i < MR::getViewerInstance().forceRedrawMinimumIncrementAfterEvents; ++i ) - MR::CommandLoop::runCommandFromGUIThread( [] {} ); // wait frame - } - - // Read/write values: (drags, sliders, etc) - - template - struct Value - { - T value = 0; - T min = 0; - T max = 0; - }; - template <> - struct Value - { - std::string value; - - std::optional> allowedValues; - }; - using ValueInt = Value; - using ValueUint = Value; - using ValueReal = Value; - using ValueString = Value; + MR::CommandLoop::runCommandFromGUIThread( [] {} ); // Wait a few frames. + }, + "Simulate a button click. Use `uiListEntries()` to find button names." +) +namespace +{ template - Value readValue( const std::vector& path ) + Control::Value readValue( const std::vector& path ) { - if ( path.empty() ) - throw std::runtime_error( "Empty path not allowed here." ); - Value ret; - MR::pythonAppendOrRun( [&] + Control::Value ret; + MR::CommandLoop::runCommandFromGUIThread( [&] { - const auto& group = findGroup( { path.data(), path.size() - 1 } ); - auto iter = group.elems.find( path.back() ); - if ( iter == group.elems.end() ) - throw std::runtime_error( fmt::format( "No such entry: `{}`. Known entries are: {}.", path.back(), listKeys( group ) ) ); - const auto& entry = *MR::expectedValueOrThrow( iter->second.getAs( path.back() ) ); - - if constexpr ( std::is_same_v ) - { - if ( auto val = std::get_if>( &entry.value ) ) - { - ret.value = val->value; - ret.allowedValues = val->allowedValues; - return; - } - - throw std::runtime_error( "This isn't a string." ); - } - else - { - // Try to read with the wrong signedness first. - if constexpr ( std::is_same_v ) - { - if ( auto val = std::get_if>( &entry.value ) ) - { - // Allow if the value is not too large. - // We don't check if the max bound is too large, because it be too large by default if not specified. - - if ( val->value > std::uint64_t( std::numeric_limits::max() ) ) - throw std::runtime_error( "Attempt to read an uint64_t value as an int64_t, but the value is too large to fit into the target type. Read as uint64_t instead." ); - ret.value = std::int64_t( val->value ); - ret.min = std::int64_t( std::min( val->min, std::uint64_t( std::numeric_limits::max() ) ) ); - ret.max = std::int64_t( std::min( val->max, std::uint64_t( std::numeric_limits::max() ) ) ); - return; - } - } - else if constexpr ( std::is_same_v ) - { - if ( auto val = std::get_if>( &entry.value ) ) - { - // Allow if the value is nonnegative, and the min bound is also nonnegative. - - if ( val->value < 0 || val->min < 0 ) - throw std::runtime_error( "Attempt to read an int64_t value as a uint64_t, but it is or can be negative. Read as int64_t instead." ); - ret.value = std::uint64_t( val->value ); - ret.min = std::uint64_t( val->min ); - ret.max = std::uint64_t( val->max ); - return; - } - } - - if ( auto val = std::get_if>( &entry.value ) ) - { - ret.value = val->value; - ret.min = val->min; - ret.max = val->max; - return; - } - - throw std::runtime_error( std::is_floating_point_v - ? "This isn't a floating-point value." - : "This isn't an integer." - ); - } + ret = MR::expectedValueOrThrow( Control::readValue( path ) ); } ); return ret; } - - template - void writeValue( const std::vector& path, T value ) - { - if constexpr ( WarnDeprecated ) - std::fprintf(stderr, "This function is deprecated, please use the overloaded `uiWriteValue()` instead."); - - if ( path.empty() ) - throw std::runtime_error( "writeValue: empty path not allowed here." ); - - const std::string pathString = stringVectorToString( path ); - spdlog::info( "writeValue {} = {}, frame {}", pathString, value, MR::getViewerInstance().getTotalFrames() ); - - MR::pythonAppendOrRun( [&] - { - const auto& group = findGroup( { path.data(), path.size() - 1 } ); - auto iter = group.elems.find( path.back() ); - if ( iter == group.elems.end() ) - throw std::runtime_error( fmt::format( "writeValue {}: no such entry: `{}`. Known entries are: {}.", pathString, path.back(), listKeys( group ) ) ); - const auto& entry = *MR::expectedValueOrThrow( iter->second.getAs( path.back() ) ); - - auto writeValueOfCorrectType = [&entry, &pathString]( auto fixedValue ) - { - using U = decltype( fixedValue ); - auto &target = std::get>( entry.value ); - - // Validate the value. - if constexpr ( std::is_same_v ) - { - if ( target.allowedValues && std::find( target.allowedValues->begin(), target.allowedValues->end(), fixedValue ) == target.allowedValues->end() ) - throw std::runtime_error( fmt::format( "writeValue {}: string `{}` is not allowed here. Allowew values: {}.", pathString, fixedValue, stringVectorToString( *target.allowedValues ) ) ); - } - else - { - if ( fixedValue < target.min ) - throw std::runtime_error( fmt::format( "writeValue {}: the specified value {} is less than the min bound {}.", pathString, fixedValue, target.min ) ); - if ( fixedValue > target.max ) - throw std::runtime_error( fmt::format( "writeValue {}: the specified value {} is more than the max bound {}.", pathString, fixedValue, target.max ) ); - } - - std::get>( entry.value ).simulatedValue = std::move( fixedValue ); - }; - - if constexpr ( std::is_same_v ) - { - if ( std::holds_alternative>( entry.value ) ) - writeValueOfCorrectType( std::move( value ) ); - else - throw std::runtime_error( fmt::format( "writeValue: `{}` is a number, but received a string.", pathString ) ); - } - else if constexpr ( std::is_same_v ) - { - std::visit( MR::overloaded{ - [&]( const TestEngine::ValueEntry::Value& ){ throw std::runtime_error( fmt::format( "writeValue: `{}` is a string, but received a number.", pathString ) ); }, - [&]( const TestEngine::ValueEntry::Value& ){ writeValueOfCorrectType( value ); }, - [&]( const TestEngine::ValueEntry::Value& ){ throw std::runtime_error( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathString ) ); }, - [&]( const TestEngine::ValueEntry::Value& ){ throw std::runtime_error( fmt::format( "writeValue: `{}` is an integer, but received a fractional number.", pathString ) ); }, - }, entry.value ); - } - else if constexpr ( std::is_same_v ) - { - std::visit( MR::overloaded{ - [&]( const TestEngine::ValueEntry::Value& ){ throw std::runtime_error( fmt::format( "writeValue: `{}` is a string, but received a number.", pathString ) ); }, - [&]( const TestEngine::ValueEntry::Value& ){ writeValueOfCorrectType( double( value ) ); }, - [&]( const TestEngine::ValueEntry::Value& ){ writeValueOfCorrectType( value ); }, - [&]( const TestEngine::ValueEntry::Value& ) - { - if ( value < 0 ) - throw std::runtime_error( fmt::format( "writeValue: `{}` is unsigned, but received a negative number.", pathString ) ); - writeValueOfCorrectType( std::uint64_t( value ) ); - }, - }, entry.value ); - } - else if constexpr ( std::is_same_v ) - { - std::visit( MR::overloaded{ - [&]( const TestEngine::ValueEntry::Value& ){ throw std::runtime_error( fmt::format( "writeValue: `{}` is a string, but received a number.", pathString ) ); }, - [&]( const TestEngine::ValueEntry::Value& ){ writeValueOfCorrectType( double( value ) ); }, - [&]( const TestEngine::ValueEntry::Value& ){ writeValueOfCorrectType( value ); }, - [&]( const TestEngine::ValueEntry::Value& ) - { - if ( value > std::uint64_t( std::numeric_limits::max() ) ) - throw std::runtime_error( fmt::format( "writeValue: `{}` is signed, but received an unsigned integer large enough to not be representable as `int64_t`.", pathString ) ); - writeValueOfCorrectType( std::int64_t( value ) ); - }, - }, entry.value ); - } - } ); - } } -MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiEntry, TypedEntry ) -MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueInt, ValueInt ) -MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueUint, ValueUint ) -MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueReal, ValueReal ) -MR_ADD_PYTHON_CUSTOM_CLASS( mrviewerpy, UiValueString, ValueString ) - -MR_ADD_PYTHON_CUSTOM_DEF( mrviewerpy, UiEntry, [] ( pybind11::module_& m ) -{ - pybind11::enum_( m, "UiEntryType", "UI entry type enum." ) - .value( "button", EntryType::button ) - .value( "group", EntryType::group ) - .value( "valueInt", EntryType::valueInt ) - .value( "valueUint", EntryType::valueUint ) - .value( "valueReal", EntryType::valueReal ) - .value( "valueString", EntryType::valueString ) - .value( "other", EntryType::other ) - ; - - MR_PYTHON_CUSTOM_CLASS( UiValueInt ).def_readonly( "value", &ValueInt::value ).def_readonly( "min", &ValueInt::min ).def_readonly( "max", &ValueInt::max ); - MR_PYTHON_CUSTOM_CLASS( UiValueUint ).def_readonly( "value", &ValueUint::value ).def_readonly( "min", &ValueUint::min ).def_readonly( "max", &ValueUint::max ); - MR_PYTHON_CUSTOM_CLASS( UiValueReal ).def_readonly( "value", &ValueReal::value ).def_readonly( "min", &ValueReal::min ).def_readonly( "max", &ValueReal::max ); - MR_PYTHON_CUSTOM_CLASS( UiValueString ).def_readonly( "value", &ValueString::value ).def_readonly( "allowed", &ValueString::allowedValues ); - - MR_PYTHON_CUSTOM_CLASS( UiEntry ) - .def_readonly( "name", &TypedEntry::name ) - .def_readonly( "type", &TypedEntry::type ) - .def("__repr__", []( const TypedEntry& e ) - { - const char* typeString = nullptr; - switch ( e.type ) - { - case EntryType::button: typeString = "button"; break; - case EntryType::valueInt: typeString = "valueInt"; break; - case EntryType::valueUint: typeString = "valueUint"; break; - case EntryType::valueReal: typeString = "valueReal"; break; - case EntryType::valueString: typeString = "valueString"; break; - case EntryType::group: typeString = "group"; break; - case EntryType::other: typeString = "other"; break; - } - assert( typeString && "Unknown enum." ); - if ( !typeString ) - typeString = "??"; - - return fmt::format( "", e.name, typeString ); - } ) - ; -} ) - -MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiListEntries, listEntries, - "List existing UI entries at the specified path.\n" - "Pass an empty list to see top-level groups.\n" - "Add group name to the end of the vector to see its contents.\n" - "When you find the button you need, pass it to `uiPressButton()`." -) -MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiPressButton, pressButton, - "Simulate a button click. Use `uiListEntries()` to find button names." -) - MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiReadValueInt, readValue, "Read a value from a drag/slider widget. This function is for signed integers." ) @@ -381,6 +98,18 @@ MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiReadValueString, readValue, "Read a value from a drag/slider widget. This function is for strings." ) +namespace +{ + template + void writeValue( const std::vector& path, T value ) + { + MR::CommandLoop::runCommandFromGUIThread( [&] + { + MR::expectedValueOrThrow( Control::writeValue( path, std::move( value ) ) ); + } ); + } +} + MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiWriteValue, writeValue, "Write a value to a drag/slider widget. This overload is for signed integers." ) @@ -394,17 +123,4 @@ MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiWriteValue, writeValue, "Write a value to a drag/slider widget. This overload is for strings." ) -// Those are deprecated and print a warning when called. Prefer the overlaoded `uiWriteValue()` above. -MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiWriteValueInt, (writeValue), - "Write a value to a drag/slider widget. This overload is for signed integers." -) -MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiWriteValueUint, (writeValue), - "Write a value to a drag/slider widget. This overload is for unsigned integers." -) -MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiWriteValueReal, (writeValue), - "Write a value to a drag/slider widget. This overload is for real numbers." -) -MR_ADD_PYTHON_FUNCTION( mrviewerpy, uiWriteValueString, (writeValue), - "Write a value to a drag/slider widget. This overload is for strings." -) // ] end deprecated From 2f7e1c6dec5208d7975b62aa15e7b6f5e0ca8df0 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Wed, 8 Apr 2026 09:00:50 -0500 Subject: [PATCH 02/45] Some more refactoring. --- scripts/ask_emscripten_mode.src | 40 +++++++++++++++++++++++++++++++++ scripts/build_source.sh | 34 ++-------------------------- scripts/build_thirdparty.sh | 32 ++------------------------ 3 files changed, 44 insertions(+), 62 deletions(-) create mode 100644 scripts/ask_emscripten_mode.src diff --git a/scripts/ask_emscripten_mode.src b/scripts/ask_emscripten_mode.src new file mode 100644 index 000000000000..093069c6c924 --- /dev/null +++ b/scripts/ask_emscripten_mode.src @@ -0,0 +1,40 @@ +#!/bin/false +# This file is supposed to be sourced. + +[[ -v MR_EMSCRIPTEN_SINGLETHREAD ]] || export MR_EMSCRIPTEN_SINGLETHREAD=0 +[[ -v MR_EMSCRIPTEN_WASM64 ]] || export MR_EMSCRIPTEN_WASM64=0 + +if [[ $OSTYPE == "linux"* ]]; then + if [ ! -n "$MR_EMSCRIPTEN" ]; then + read -t 5 -p "Build with emscripten? Press (y) in 5 seconds to build (y/s/l/N) (s - singlethreaded, l - 64-bit)" -rsn 1 + echo; + case $REPLY in + Y|y) + export MR_EMSCRIPTEN="ON";; + S|s) + export MR_EMSCRIPTEN="ON" + export MR_EMSCRIPTEN_SINGLETHREAD=1;; + L|l) + export MR_EMSCRIPTEN="ON" + export MR_EMSCRIPTEN_WASM64=1;; + *) + export MR_EMSCRIPTEN="OFF";; + esac + fi +else + if [ ! -n "$MR_EMSCRIPTEN" ]; then + MR_EMSCRIPTEN="OFF" + fi +fi + +# Normalize the spelling of some variables. +if [ $MR_EMSCRIPTEN == "ON" ]; then + if [[ $MR_EMSCRIPTEN_SINGLE == "ON" ]]; then + MR_EMSCRIPTEN_SINGLETHREAD=1 + fi + if [[ $MR_EMSCRIPTEN_WASM64 == "ON" ]]; then + MR_EMSCRIPTEN_WASM64=1 + fi +fi + +echo "Emscripten ${MR_EMSCRIPTEN}, singlethread ${MR_EMSCRIPTEN_SINGLETHREAD}, 64-bit ${MR_EMSCRIPTEN_WASM64}" diff --git a/scripts/build_source.sh b/scripts/build_source.sh index fae3d50dbf5c..eef41a1500a0 100755 --- a/scripts/build_source.sh +++ b/scripts/build_source.sh @@ -8,39 +8,9 @@ logfile="`pwd`/build_source_${dt}.log" echo "Project build script started." echo "You could find output in ${logfile}" -MR_EMSCRIPTEN_SINGLETHREAD=0 -if [[ $OSTYPE == "linux"* ]]; then - if [ ! -n "$MR_EMSCRIPTEN" ]; then - read -t 5 -p "Build with emscripten? Press (y) in 5 seconds to build (y/s/l/N) (s - singlethreaded, l - 64-bit)" -rsn 1 - echo; - case $REPLY in - Y|y) - MR_EMSCRIPTEN="ON";; - S|s) - MR_EMSCRIPTEN="ON" - MR_EMSCRIPTEN_SINGLETHREAD=1;; - L|l) - MR_EMSCRIPTEN="ON" - MR_EMSCRIPTEN_WASM64=1;; - *) - MR_EMSCRIPTEN="OFF";; - esac - fi -else - if [ ! -n "$MR_EMSCRIPTEN" ]; then - MR_EMSCRIPTEN="OFF" - fi -fi -echo "Emscripten ${MR_EMSCRIPTEN}, singlethread ${MR_EMSCRIPTEN_SINGLETHREAD}, 64-bit ${MR_EMSCRIPTEN_WASM64}" +SCRIPT_DIR="$(dirname "$BASH_SOURCE")" -if [ $MR_EMSCRIPTEN == "ON" ]; then - if [[ $MR_EMSCRIPTEN_SINGLE == "ON" ]]; then - MR_EMSCRIPTEN_SINGLETHREAD=1 - fi - if [[ $MR_EMSCRIPTEN_WASM64 == "ON" ]]; then - MR_EMSCRIPTEN_WASM64=1 - fi -fi +. "$SCRIPT_DIR/ask_emscripten_mode.src" if [ ! -n "$MESHLIB_BUILD_RELEASE" ]; then read -t 5 -p "Build MeshLib Release? Press (n) in 5 seconds to cancel (Y/n)" -rsn 1 diff --git a/scripts/build_thirdparty.sh b/scripts/build_thirdparty.sh index a21bda8993e2..5fa25480431b 100755 --- a/scripts/build_thirdparty.sh +++ b/scripts/build_thirdparty.sh @@ -32,38 +32,10 @@ else echo "Host system: ${OSTYPE}" fi -MR_EMSCRIPTEN_SINGLETHREAD=0 -if [[ $OSTYPE == "linux"* ]] && [ "${MR_STATE}" != "DOCKER_BUILD" ]; then - if [ ! -n "$MR_EMSCRIPTEN" ]; then - read -t 5 -p "Build with emscripten? Press (y) in 5 seconds to build (y/s/l/N) (s - singlethreaded, l - 64-bit)" -rsn 1 - echo; - case $REPLY in - Y|y) - MR_EMSCRIPTEN="ON";; - S|s) - MR_EMSCRIPTEN="ON" - MR_EMSCRIPTEN_SINGLETHREAD=1;; - L|l) - MR_EMSCRIPTEN="ON" - MR_EMSCRIPTEN_WASM64=1;; - *) - MR_EMSCRIPTEN="OFF";; - esac - fi -else - if [ ! -n "$MR_EMSCRIPTEN" ]; then - MR_EMSCRIPTEN="OFF" - fi -fi -echo "Emscripten ${MR_EMSCRIPTEN}, singlethread ${MR_EMSCRIPTEN_SINGLETHREAD}, 64-bit ${MR_EMSCRIPTEN_WASM64}" +. "$SCRIPT_DIR/ask_emscripten_mode.src" if [ $MR_EMSCRIPTEN == "ON" ]; then - if [[ $MR_EMSCRIPTEN_SINGLE == "ON" ]]; then - MR_EMSCRIPTEN_SINGLETHREAD=1 - fi - if [[ $MR_EMSCRIPTEN_WASM64 == "ON" ]]; then - MR_EMSCRIPTEN_WASM64=1 - fi + true # Nothing. elif [ -n "${INSTALL_REQUIREMENTS}" ]; then echo "Check requirements. Running ${INSTALL_REQUIREMENTS} ..." ${SCRIPT_DIR}/$INSTALL_REQUIREMENTS From bd3ee1aac730b1c4486713ddb4bcfdca53b3e6fb Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Wed, 8 Apr 2026 09:22:59 -0500 Subject: [PATCH 03/45] Update VS projects. --- source/MRViewer/MRViewer.vcxproj | 4 +++- source/MRViewer/MRViewer.vcxproj.filters | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/source/MRViewer/MRViewer.vcxproj b/source/MRViewer/MRViewer.vcxproj index e31e4760b615..0f3ccc4ef07c 100644 --- a/source/MRViewer/MRViewer.vcxproj +++ b/source/MRViewer/MRViewer.vcxproj @@ -159,6 +159,7 @@ + @@ -338,6 +339,7 @@ + @@ -560,4 +562,4 @@ - \ No newline at end of file + diff --git a/source/MRViewer/MRViewer.vcxproj.filters b/source/MRViewer/MRViewer.vcxproj.filters index 32889f4ad183..e741a7ce3db3 100644 --- a/source/MRViewer/MRViewer.vcxproj.filters +++ b/source/MRViewer/MRViewer.vcxproj.filters @@ -382,6 +382,9 @@ UIStyle + + UIStyle + Viewer @@ -882,6 +885,9 @@ UIStyle + + UIStyle + Viewer @@ -1252,4 +1258,4 @@ resource\independent_icons\X3 - \ No newline at end of file + From b21364725f9bb49aa45aebd52cda4ca9618de074 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 9 Apr 2026 05:28:49 -0500 Subject: [PATCH 04/45] Hopefully fix mac builds. --- scripts/ask_emscripten_mode.src | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ask_emscripten_mode.src b/scripts/ask_emscripten_mode.src index 093069c6c924..5772897f77d2 100644 --- a/scripts/ask_emscripten_mode.src +++ b/scripts/ask_emscripten_mode.src @@ -1,8 +1,8 @@ #!/bin/false # This file is supposed to be sourced. -[[ -v MR_EMSCRIPTEN_SINGLETHREAD ]] || export MR_EMSCRIPTEN_SINGLETHREAD=0 -[[ -v MR_EMSCRIPTEN_WASM64 ]] || export MR_EMSCRIPTEN_WASM64=0 +[[ ${MR_EMSCRIPTEN_SINGLETHREAD:=} ]] || export MR_EMSCRIPTEN_SINGLETHREAD=0 +[[ ${MR_EMSCRIPTEN_WASM64:=} ]] || export MR_EMSCRIPTEN_WASM64=0 if [[ $OSTYPE == "linux"* ]]; then if [ ! -n "$MR_EMSCRIPTEN" ]; then From 87778f9f5cc0330ac12c5ce252b94f1758f5e035 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 9 Apr 2026 05:39:30 -0500 Subject: [PATCH 05/45] Try again. --- scripts/ask_emscripten_mode.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ask_emscripten_mode.src b/scripts/ask_emscripten_mode.src index 5772897f77d2..5179cf27d764 100644 --- a/scripts/ask_emscripten_mode.src +++ b/scripts/ask_emscripten_mode.src @@ -4,7 +4,7 @@ [[ ${MR_EMSCRIPTEN_SINGLETHREAD:=} ]] || export MR_EMSCRIPTEN_SINGLETHREAD=0 [[ ${MR_EMSCRIPTEN_WASM64:=} ]] || export MR_EMSCRIPTEN_WASM64=0 -if [[ $OSTYPE == "linux"* ]]; then +if [[ $OSTYPE == "linux"* && $MR_STATE != "DOCKER_BUILD" ]]; then if [ ! -n "$MR_EMSCRIPTEN" ]; then read -t 5 -p "Build with emscripten? Press (y) in 5 seconds to build (y/s/l/N) (s - singlethreaded, l - 64-bit)" -rsn 1 echo; From a1e2e27a591f539d5c64f5b14901291d192b2234 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 9 Apr 2026 10:47:51 -0500 Subject: [PATCH 06/45] Add missing header. --- source/MRViewer/MRUITestEngineControl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/source/MRViewer/MRUITestEngineControl.cpp b/source/MRViewer/MRUITestEngineControl.cpp index 4b5afbdd2824..8c8254bb11f9 100644 --- a/source/MRViewer/MRUITestEngineControl.cpp +++ b/source/MRViewer/MRUITestEngineControl.cpp @@ -4,6 +4,7 @@ #include "MRPch/MRFmt.h" #include "MRViewer/MRUITestEngine.h" +#include #include #include #include From e4a9a213f2a9ae08040b51e2fbc6333ee71dda6d Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 06:01:24 -0500 Subject: [PATCH 07/45] Move the MCP server to MeshLib. --- .gitmodules | 9 + source/MRViewer/CMakeLists.txt | 19 + source/MRViewer/MRMcp.cpp | 427 +++++++++++++++++++++++ source/MRViewer/MRMcp.h | 48 +++ source/MRViewer/MRViewer.vcxproj | 9 +- source/MRViewer/MRViewer.vcxproj.filters | 9 + source/MeshLib.sln | 7 + source/fastmcpp/CMakeLists.txt | 20 ++ source/fastmcpp/fastmcpp.vcxproj | 214 ++++++++++++ thirdparty/cpp-httplib | 1 + thirdparty/fastmcpp | 1 + thirdparty/nlohmann-json | 1 + 12 files changed, 763 insertions(+), 2 deletions(-) create mode 100644 source/MRViewer/MRMcp.cpp create mode 100644 source/MRViewer/MRMcp.h create mode 100644 source/fastmcpp/CMakeLists.txt create mode 100644 source/fastmcpp/fastmcpp.vcxproj create mode 160000 thirdparty/cpp-httplib create mode 160000 thirdparty/fastmcpp create mode 160000 thirdparty/nlohmann-json diff --git a/.gitmodules b/.gitmodules index 5e84140c007d..ef9d7a0d5172 100644 --- a/.gitmodules +++ b/.gitmodules @@ -76,3 +76,12 @@ [submodule "thirdparty/OpenCTM-git"] path = thirdparty/OpenCTM-git url = https://github.com/MeshInspector/OpenCTM.git +[submodule "thirdparty/fastmcpp"] + path = thirdparty/fastmcpp + url = https://github.com/MeshInspector/fastmcpp +[submodule "thirdparty/nlohmann-json"] + path = thirdparty/nlohmann-json + url = https://github.com/nlohmann/json +[submodule "thirdparty/cpp-httplib"] + path = thirdparty/cpp-httplib + url = https://github.com/yhirose/cpp-httplib diff --git a/source/MRViewer/CMakeLists.txt b/source/MRViewer/CMakeLists.txt index 04b9e0e332b1..d661235bf559 100644 --- a/source/MRViewer/CMakeLists.txt +++ b/source/MRViewer/CMakeLists.txt @@ -111,6 +111,25 @@ IF(MR_PCH) target_precompile_headers(${PROJECT_NAME} REUSE_FROM MRPch) ENDIF() +# Fastmcpp stuff. +option(MESHLIB_BUILD_MCP "Enable MCP server" ON) +IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) + # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. + IF(MESHLIB_USE_VCPKG) + add_subdirectory(../fastmcpp fastmcpp) + ENDIF() + + target_link_libraries(${PROJECT_NAME} PRIVATE fastmcpp_core) + + target_include_directories(${PROJECT_NAME} PRIVATE + ${MESHINSPECTOR_THIRDPARTY_DIR}/fastmcpp/include + ${MESHINSPECTOR_THIRDPARTY_DIR}/cpp-httplib + ${MESHINSPECTOR_THIRDPARTY_DIR}/nlohmann-json/include + ) +ELSE() + target_compile_definitions(${PROJECT_NAME} PRIVATE MR_ENABLE_MCP_SERVER=0) +ENDIF() + file(GLOB JSONS "*.json") file(GLOB AWESOME_FONTS "${MESHLIB_THIRDPARTY_DIR}/fontawesome-free/*.ttf") file(GLOB IMGUI_FONTS "${MESHLIB_THIRDPARTY_DIR}/imgui/misc/fonts/*.ttf") diff --git a/source/MRViewer/MRMcp.cpp b/source/MRViewer/MRMcp.cpp new file mode 100644 index 000000000000..f8d8215c7016 --- /dev/null +++ b/source/MRViewer/MRMcp.cpp @@ -0,0 +1,427 @@ +#include "MRMcp.h" + +#if MR_ENABLE_MCP_SERVER +#include "MRMesh/MRSystem.h" +#include "MRPch/MRSpdlog.h" +#include "MRViewer/MRCommandLoop.h" +#include "MRViewer/MRUITestEngineControl.h" +#include "MRViewer/MRViewer.h" + +#undef _t // Our translation macro interefers with Fastmcpp. + +#if defined( __GNUC__ ) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#elif defined( _MSC_VER ) +#pragma warning( push ) +#pragma warning( disable: 4100 ) // unreferenced formal parameter +#pragma warning( disable: 4355 ) // 'this': used in base member initializer list +#endif + +#include +#include + +#if defined( __GNUC__ ) +#pragma GCC diagnostic pop +#elif defined( _MSC_VER ) +#pragma warning( pop ) +#endif + +#include +#endif + + +/* HOW TO TEST MCP: + +Use "MCP Inspector". Run it with `npx @modelcontextprotocol/inspector`, where `npx` is installed as a part of Node.JS. +Set: + Transport Type = SSE + URL = http://localhost:8080/sse + Connection Type = Via Proxy (Doesn't work for me without proxy now when we're using the Fastmcpp library, but did work with another library; not sure why.) + +Press `Connect`. +Press `List Tools` (if grayed out, do `Clear` first). +Click on your tool. +On the right panel, set parameters. + For some parameter types, it helps to press `Switch to JSON` on the right, then type them as JSON. +Press `Run Tool`. + Note, you might need to press this twice. In some cases, the first press passes stale/empty parameters. +Then check for validation errors, below this button. + +If your output doesn't match the schema you specified, paste both the output and the schema (using the `Copy` button in the top-right corner of the code blocks; that copies JSON properly, unlike Ctrl+C in this case) + into a schema validator, e.g. https://www.jsonschemavalidator.net/ + +*/ + + +namespace MR +{ + +struct McpServer::State +{ + #if MR_ENABLE_MCP_SERVER + fastmcpp::tools::ToolManager tool_manager; // This has to be persistent, or `fastmcpp::mcp::make_mcp_handler()` dangles it. + fastmcpp::server::SseServerWrapper server; + + + State( int port, std::string name, std::string version, fastmcpp::tools::ToolManager new_tool_manager, const std::unordered_map& tool_descs ) + : tool_manager( std::move( new_tool_manager ) ), + server( fastmcpp::mcp::make_mcp_handler( name, version, tool_manager, tool_descs ), "127.0.0.1", port ) + {} + #endif +}; + +McpServer::McpServer() + : port_( 7887 ) // An arbirary value. +{ + recreateServer(); +} + +McpServer::McpServer( McpServer&& ) = default; +McpServer& McpServer::operator=( McpServer&& ) = default; +McpServer::~McpServer() = default; + +void McpServer::recreateServer() +{ + #if MR_ENABLE_MCP_SERVER + + fastmcpp::tools::ToolManager tool_manager; + std::unordered_map tool_descs; + + auto addTool = [&]( std::string id, std::string name, std::string desc, fastmcpp::Json input_schema, fastmcpp::Json output_schema, fastmcpp::tools::Tool::Fn func ) + { + tool_descs.try_emplace( id, desc ); // Why is this not automated by the library? + tool_manager.register_tool( fastmcpp::tools::Tool( id, input_schema, output_schema, func, name, desc, {} ) ); + }; + + static const auto skipFramesAfterInput = [] + { + for ( int i = 0; i < MR::getViewerInstance().forceRedrawMinimumIncrementAfterEvents; ++i ) + MR::CommandLoop::runCommandFromGUIThread( [] {} ); // Wait a few frames. + }; + + addTool( + /*id*/"ui.listEntries", + /*name*/"List UI entries", + /*desc*/"Returns the list of UI elements at the given path. The elements form a tree. Pass an empty array to get the top-level elements. Each element is described by a string. The path parameter describes the path from the root node to a specific element. Only elements of type `group` can have sub-elements.", + /*input_schema*/fastmcpp::Json::object( { + { "type", "object" }, + { "properties", fastmcpp::Json::object( { + { "path", fastmcpp::Json::object( { + { "type", "array" }, + { "items", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + } ) } + } ) }, + { "required", fastmcpp::Json::array( { + "path", + } ) }, + } ), + /*output_schema*/fastmcpp::Json::object( { + { "type", "array" }, + { "items", fastmcpp::Json::object( { + { "type", "object" }, + { "properties", fastmcpp::Json::object( { + { "name", fastmcpp::Json::object( { + { "type", "string" }, + } ) }, + { "type", fastmcpp::Json::object( { + { "type", "string" }, + } ) }, + } ) }, + { "required", fastmcpp::Json::array( { + "name", + "type", + } ) }, + } ) }, + } ), + /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json + { + if ( !params.contains( "path" ) ) + throw std::runtime_error( "The path parameter is missing." ); + + std::vector list; + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::listEntries( params["path"].get>() ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + list = std::move( *ex ); + } ); + + fastmcpp::Json ret = fastmcpp::Json::array(); + for ( const auto& elem : list ) + { + std::string typeStr; + switch ( elem.type ) + { + case UI::TestEngine::Control::EntryType::button: typeStr = "button"; break; + case UI::TestEngine::Control::EntryType::group: typeStr = "group"; break; + case UI::TestEngine::Control::EntryType::valueInt: typeStr = "int"; break; + case UI::TestEngine::Control::EntryType::valueUint: typeStr = "uint"; break; + case UI::TestEngine::Control::EntryType::valueReal: typeStr = "float"; break; // Hopefully "float" is more clear to LLMs than "real". The actual underlying type is `double`. + case UI::TestEngine::Control::EntryType::valueString: typeStr = "string"; break; + } + + assert( !typeStr.empty() ); + if ( typeStr.empty() ) + typeStr = "invalid"; + + ret.push_back( fastmcpp::Json::object( { + { "name", elem.name }, + { "type", std::move( typeStr ) }, + } ) ); + } + + return fastmcpp::Json::object( { { "result", ret } } ); + } + ); + + addTool( + /*id*/"ui.pressButton", + /*name*/"Press button", + /*desc*/"Presses the button at the given path.", + /*input_schema*/fastmcpp::Json::object( { + { "type", "object" }, + { "properties", fastmcpp::Json::object( { + { "path", fastmcpp::Json::object( { + { "type", "array" }, + { "items", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + } ) }, + } ) }, + { "required", fastmcpp::Json::array( { + "path", + } ) }, + } ), + /*output_schema*/fastmcpp::Json::object(), + /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json + { + if ( !params.contains( "path" ) ) + throw std::runtime_error( "The path parameter is missing." ); + + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::pressButton( params["path"].get>() ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + } ); + skipFramesAfterInput(); + + return fastmcpp::Json::object(); + } + ); + + auto handleValueType = [&]( const std::string& typeName ) + { + addTool( + /*id*/"ui.readValue" + typeName, + /*name*/"Read " + typeName + " value", + /*desc*/"Reads the value at the given path, of type `" + typeName + "`." + + ( + std::is_same_v + ? + " If the result contains an array called `allowedValues`, then when assigning a new value using `ui.writeValue" + typeName + "`, it must match one of the strings listed in `allowedValues`." + : + " When assigning a new value using `ui.writeValue" + typeName + "`, it must be between `min` and `max` inclusive." + ), + /*input_schema*/fastmcpp::Json::object( { + { "type", "object" }, + { "properties", fastmcpp::Json::object( { + { "path", fastmcpp::Json::object( { + { "type", "array" }, + { "items", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + } ) }, + } ) }, + { "required", fastmcpp::Json::array( { + "path", + } ) }, + } ), + /*output_schema*/( + std::is_same_v + ? + fastmcpp::Json::object( { + { "type", "object" }, + { "properties", fastmcpp::Json::object( { + { "value", fastmcpp::Json::object( { + { "type", "string" }, + } ) }, + { "allowedValues", fastmcpp::Json::object( { + { "type", "array" }, + { "items", fastmcpp::Json::object( { + { "type", "string" }, + } ) }, + } ) }, + } ) }, + { "required", fastmcpp::Json::array( { + "value", + } ) }, + } ) + : + fastmcpp::Json::object( { + { "type", "object" }, + { "properties", fastmcpp::Json::object( { + { "value", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + { "min", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + { "max", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + } ) }, + { "required", fastmcpp::Json::array( { + "value", + "min", + "max", + } ) }, + } ) + ), + /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json + { + if ( !params.contains( "path" ) ) + throw std::runtime_error( "The path parameter is missing." ); + + UI::TestEngine::Control::Value value; + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::readValue( params["path"].get>() ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + value = std::move( *ex ); + } ); + + fastmcpp::Json ret = fastmcpp::Json::object(); + ret["value"] = value.value; + if constexpr ( std::is_same_v ) + { + if ( value.allowedValues ) + ret["allowedValues"] = *value.allowedValues; + } + else + { + ret["min"] = value.min; + ret["max"] = value.max; + } + + return ret; + } + ); + + addTool( + /*id*/"ui.writeValue" + typeName, + /*name*/"Write " + typeName + " value", + /*desc*/"Writes the value at the given path, of type `" + typeName + "`. You can call `ui.readValue" + typeName + "` before this to know what values are allowed.", + /*input_schema*/fastmcpp::Json::object( { + { "type", "object" }, + { "properties", fastmcpp::Json::object( { + { "path", fastmcpp::Json::object( { + { "type", "array" }, + { "items", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + } ) }, + { "value", fastmcpp::Json::object( { + { "type", "number" }, + } ) }, + } ) }, + { "required", fastmcpp::Json::array( { + "path", + "value", + } ) }, + } ), + /*output_schema*/fastmcpp::Json::object(), + /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json + { + if ( !params.contains( "path" ) ) + throw std::runtime_error( "The path parameter is missing." ); + + if ( !params.contains( "path" ) ) + throw std::runtime_error( "The path parameter is missing." ); + if ( !params.contains( "value" ) ) + throw std::runtime_error( "The value parameter is missing." ); + + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::writeValue( params["path"].get>(), T( params["value"] ) ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + } ); + skipFramesAfterInput(); + + return fastmcpp::Json::object(); + } + ); + }; + + handleValueType.operator()( "Int" ); + handleValueType.operator()( "Uint" ); + handleValueType.operator()( "Real" ); + handleValueType.operator()( "String" ); + + state_ = std::make_unique( port_, "MeshInspector", GetMRVersionString(), std::move( tool_manager ), tool_descs ); + + #endif +} + +bool McpServer::isRunning() const +{ + #if MR_ENABLE_MCP_SERVER + return state_->server.running(); + #else + return false; + #endif +} + +bool McpServer::setRunning( bool enable ) +{ + #if MR_ENABLE_MCP_SERVER + if ( enable ) + { + bool ok = state_->server.start(); + if ( ok ) + spdlog::info( "MCP server started on port {}", getPort() ); + else + spdlog::error( "MCP server failed to start on port {}", getPort() ); + return ok; + } + else + { + state_->server.stop(); + spdlog::info( "MCP server stopped" ); + return true; + } + #else + (void)enable; + return false; + #endif +} + +McpServer& getDefaultMcpServer() +{ + static McpServer ret = [] + { + McpServer server; + server.setRunning( true ); + return server; + }(); + return ret; +} + +#if MR_ENABLE_MCP_SERVER +static const std::nullptr_t init_mcp = []{ + // Poke the default MCP server to start it. + // Use `CommandLoop` to delay initialization until the viewer finishes initializing. + // Otherwise this gets called too early, before even the logger is configured. + CommandLoop::appendCommand( []{ (void)getDefaultMcpServer(); } ); + + return nullptr; +}(); +#endif + +} // namespace MR diff --git a/source/MRViewer/MRMcp.h b/source/MRViewer/MRMcp.h new file mode 100644 index 000000000000..a11d999bca27 --- /dev/null +++ b/source/MRViewer/MRMcp.h @@ -0,0 +1,48 @@ +#pragma once + +#include "exports.h" + +#include "MRViewer/MRRibbonMenuItem.h" + +#include + +#ifndef MR_ENABLE_MCP_SERVER +# ifdef __EMSCRIPTEN__ +# define MR_ENABLE_MCP_SERVER 0 +# else +# define MR_ENABLE_MCP_SERVER 1 +# endif +#endif + +namespace MR +{ + +class McpServer +{ + struct State; + + int port_ = -1; // See the constructor. + std::unique_ptr state_; + +public: + MRVIEWER_API McpServer(); + MRVIEWER_API McpServer( McpServer&& ); + MRVIEWER_API McpServer& operator=( McpServer&& ); + MRVIEWER_API ~McpServer(); + + [[nodiscard]] int getPort() const { return port_; } + void setPort( int port ) { port_ = port; } + + // This stops the server and applies the new settings, such as the port. + // Then call `setRunning( true )` to start it again. + MRVIEWER_API void recreateServer(); + + [[nodiscard]] MRVIEWER_API bool isRunning() const; + // Returns true on success, including if the server is already running and you're trying to start it again. + // Stopping always returns true. + MRVIEWER_API bool setRunning( bool enable ); +}; + +[[nodiscard]] MRVIEWER_API McpServer& getDefaultMcpServer(); + +} // namespace MR diff --git a/source/MRViewer/MRViewer.vcxproj b/source/MRViewer/MRViewer.vcxproj index a7ede43dbb4e..0baf193f0ed2 100644 --- a/source/MRViewer/MRViewer.vcxproj +++ b/source/MRViewer/MRViewer.vcxproj @@ -160,6 +160,7 @@ + @@ -341,6 +342,7 @@ + @@ -475,6 +477,9 @@ {7cc4f0fe-ace6-4441-9dd7-296066b6d69f} + + {7853aec9-a364-4587-89ae-faa9a463e6ed} + 15.0 @@ -518,7 +523,7 @@ true true $(ProjectDir)..\MRPch\MRPch.h - %(AdditionalIncludeDirectories);$(ProjectDir)..\..\thirdparty;$(ProjectDir)\..\..\thirdparty\imgui\ + %(AdditionalIncludeDirectories);$(ProjectDir)..\..\thirdparty;$(ProjectDir)\..\..\thirdparty\imgui\;..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include $(ProjectDir)..\MRPch\MRPch.h $(SolutionDir)TempOutput\MRPch\$(Platform)\$(Configuration)\MRPch.pch @@ -545,7 +550,7 @@ true $(ProjectDir)..\MRPch\MRPch.h /bigobj %(AdditionalOptions) - %(AdditionalIncludeDirectories);$(ProjectDir)..\..\thirdparty;$(ProjectDir)\..\..\thirdparty\imgui\ + %(AdditionalIncludeDirectories);$(ProjectDir)..\..\thirdparty;$(ProjectDir)\..\..\thirdparty\imgui\;..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include $(ProjectDir)..\MRPch\MRPch.h $(SolutionDir)TempOutput\MRPch\$(Platform)\$(Configuration)\MRPch.pch diff --git a/source/MRViewer/MRViewer.vcxproj.filters b/source/MRViewer/MRViewer.vcxproj.filters index c2e3b87acdb6..c1bdf077eb5e 100644 --- a/source/MRViewer/MRViewer.vcxproj.filters +++ b/source/MRViewer/MRViewer.vcxproj.filters @@ -86,6 +86,9 @@ {29097c41-4b59-45fe-9b71-20f6095be069} + + {a9f6cb49-a0c1-4ee7-bcef-760c93bdc757} + @@ -526,6 +529,9 @@ Localization + + AI + @@ -1044,6 +1050,9 @@ Localization + + AI + diff --git a/source/MeshLib.sln b/source/MeshLib.sln index 54ec44ff4fa2..74411f8f087c 100644 --- a/source/MeshLib.sln +++ b/source/MeshLib.sln @@ -62,6 +62,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MREmbeddedPython", "MREmbed EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MRTestCuda", "MRTestCuda\MRTestCuda.vcxproj", "{FFB8D063-FF1E-4F18-8479-249B36714EF7}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fastmcpp", "fastmcpp\fastmcpp.vcxproj", "{7853AEC9-A364-4587-89AE-FAA9A463E6ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -152,6 +154,10 @@ Global {FFB8D063-FF1E-4F18-8479-249B36714EF7}.Debug|x64.Build.0 = Debug|x64 {FFB8D063-FF1E-4F18-8479-249B36714EF7}.Release|x64.ActiveCfg = Release|x64 {FFB8D063-FF1E-4F18-8479-249B36714EF7}.Release|x64.Build.0 = Release|x64 + {7853AEC9-A364-4587-89AE-FAA9A463E6ED}.Debug|x64.ActiveCfg = Debug|x64 + {7853AEC9-A364-4587-89AE-FAA9A463E6ED}.Debug|x64.Build.0 = Debug|x64 + {7853AEC9-A364-4587-89AE-FAA9A463E6ED}.Release|x64.ActiveCfg = Release|x64 + {7853AEC9-A364-4587-89AE-FAA9A463E6ED}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -178,6 +184,7 @@ Global {5612E480-6980-4242-9039-BE367F4ECBF0} = {DAEF3759-BD96-475D-AA71-96ACC5279E43} {E0202297-EDB2-4CDC-9CD0-8921EFF08DA0} = {AE8B4895-7920-4AD3-B554-C858A08B1680} {FFB8D063-FF1E-4F18-8479-249B36714EF7} = {E0BE85ED-C366-40EF-8BDE-70E1EDC8860F} + {7853AEC9-A364-4587-89AE-FAA9A463E6ED} = {AE8B4895-7920-4AD3-B554-C858A08B1680} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6F7912D7-5687-4CBB-828B-1BEDD18B8249} diff --git a/source/fastmcpp/CMakeLists.txt b/source/fastmcpp/CMakeLists.txt new file mode 100644 index 000000000000..1c05cfbf0d86 --- /dev/null +++ b/source/fastmcpp/CMakeLists.txt @@ -0,0 +1,20 @@ +# This file exists so we can tweak some settings for Fastmcpp. +# If we were to `add_subdirectory` it directly from `CMakeLists.txt`, we would have to modify the global CXX flags, which is uncool. + +# Sync those flags with `scripts/build_thirdparty.sh`. + +IF(NOT WIN32) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") +ENDIF() + +IF(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4099 /wd4100 /wd4242 /wd4244 /wd4355 /wd4456 /wd4458 /wd4464 /wd4505 /wd4702 /wd5204 /wd5220 /wd5233 /wd5245") +ELSE() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-error") +ENDIF() + +set(FASTMCPP_BUILD_TESTS OFF CACHE BOOL "Build tests") +set(FASTMCPP_BUILD_EXAMPLES OFF CACHE BOOL "Build examples") +set(FASTMCPP_FETCH_CURL OFF CACHE BOOL "Fetch and build libcurl statically for POST streaming") + +add_subdirectory(${MESHINSPECTOR_THIRDPARTY_DIR}/fastmcpp fastmcpp) diff --git a/source/fastmcpp/fastmcpp.vcxproj b/source/fastmcpp/fastmcpp.vcxproj new file mode 100644 index 000000000000..9d55827c6f0b --- /dev/null +++ b/source/fastmcpp/fastmcpp.vcxproj @@ -0,0 +1,214 @@ + + + + + Debug + x64 + + + Release + x64 + + + + + + + + + + $(IntDir)client\ + + + $(IntDir)client\ + + + $(IntDir)client\ + + + $(IntDir)internal\ + + + $(IntDir)mcp\ + + + $(IntDir)mcp\ + + + $(IntDir)prompts\ + + + $(IntDir)prompts\ + + + $(IntDir)providers\ + + + $(IntDir)providers\ + + + $(IntDir)providers\ + + + $(IntDir)providers\transforms\ + + + $(IntDir)providers\transforms\ + + + $(IntDir)providers\transforms\ + + + $(IntDir)providers\transforms\ + + + $(IntDir)providers\transforms\ + + + $(IntDir)providers\transforms\ + + + $(IntDir)resources\ + + + $(IntDir)resources\ + + + $(IntDir)resources\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)server\ + + + $(IntDir)tools\ + + + $(IntDir)tools\ + + + $(IntDir)util\ + + + $(IntDir)util\ + + + $(IntDir)util\ + + + + 15.0 + {7853AEC9-A364-4587-89AE-FAA9A463E6ED} + Win32Proj + MRInterprocess + + + + StaticLibrary + true + Unicode + + + StaticLibrary + false + false + Unicode + + + + + + + + + + + + + + + + + + NotUsing + EnableAllWarnings + Disabled + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include + 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;%(DisableSpecificWarnings) + + + Console + true + %(AdditionalDependencies) + + + + + + + + + NotUsing + EnableAllWarnings + MaxSpeed + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + /bigobj %(AdditionalOptions) + %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include + 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;%(DisableSpecificWarnings) + + + Console + true + true + true + + + + + + + + + + diff --git a/thirdparty/cpp-httplib b/thirdparty/cpp-httplib new file mode 160000 index 000000000000..b045ee7f6b43 --- /dev/null +++ b/thirdparty/cpp-httplib @@ -0,0 +1 @@ +Subproject commit b045ee7f6b434a85fd011e96e28c6d4abfb18788 diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp new file mode 160000 index 000000000000..375ec467dfd1 --- /dev/null +++ b/thirdparty/fastmcpp @@ -0,0 +1 @@ +Subproject commit 375ec467dfd1aa5babcec20020ac11f41405a0d4 diff --git a/thirdparty/nlohmann-json b/thirdparty/nlohmann-json new file mode 160000 index 000000000000..394687226559 --- /dev/null +++ b/thirdparty/nlohmann-json @@ -0,0 +1 @@ +Subproject commit 3946872265598aed5a7aea68cad4d9d1f168bd4b From 68f35e50a15b6aba20e7f084c63abc6dcd2ecb01 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 06:09:47 -0500 Subject: [PATCH 08/45] Try again. --- source/MRViewer/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/MRViewer/CMakeLists.txt b/source/MRViewer/CMakeLists.txt index d661235bf559..ae8cadd4b7ba 100644 --- a/source/MRViewer/CMakeLists.txt +++ b/source/MRViewer/CMakeLists.txt @@ -122,9 +122,9 @@ IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) target_link_libraries(${PROJECT_NAME} PRIVATE fastmcpp_core) target_include_directories(${PROJECT_NAME} PRIVATE - ${MESHINSPECTOR_THIRDPARTY_DIR}/fastmcpp/include - ${MESHINSPECTOR_THIRDPARTY_DIR}/cpp-httplib - ${MESHINSPECTOR_THIRDPARTY_DIR}/nlohmann-json/include + ${MESHLIB_THIRDPARTY_DIR}/fastmcpp/include + ${MESHLIB_THIRDPARTY_DIR}/cpp-httplib + ${MESHLIB_THIRDPARTY_DIR}/nlohmann-json/include ) ELSE() target_compile_definitions(${PROJECT_NAME} PRIVATE MR_ENABLE_MCP_SERVER=0) From 8e39ecbe70425eefeea4059444e65ccd230f8bfb Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 06:49:49 -0500 Subject: [PATCH 09/45] Try again. --- thirdparty/CMakeLists.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index e4734cf29af3..d06368935de7 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -168,3 +168,19 @@ add_subdirectory(./tinygltf) set(WITH_TESTS OFF CACHE BOOL "") set(WITH_EMBIND OFF CACHE BOOL "") add_subdirectory(./laz-perf) + +IF(NOT MR_EMSCRIPTEN) + # Nlohmann-json is a dependency of fastmcpp. + set(BUILD_TESTING OFF) + add_subdirectory(./nlohmann-json) + + # Cpp-httplib is a dependency of fastmcpp. + set(HTTPLIB_TEST OFF) + add_subdirectory(./cpp-httplib) + + set(FASTMCPP_BUILD_TESTS OFF) + set(FASTMCPP_BUILD_EXAMPLES OFF) + set(FASTMCPP_FETCH_CURL OFF) + # Need a custom subdirectory name here, because it conflicts with the executable named `fastmcpp`. I'm not sure why we install stuff to the build directory. + add_subdirectory(./fastmcpp ./fastmcpp_build) +ENDIF() From cd1b76c246f379e1eeeea010d86111d4010966b3 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 07:29:56 -0500 Subject: [PATCH 10/45] Try again. --- scripts/build_thirdparty.sh | 7 +++++++ scripts/thirdparty/cpp-httplib.sh | 13 +++++++++++++ scripts/thirdparty/fastmcpp.sh | 19 +++++++++++++++++++ scripts/thirdparty/nlohmann-json.sh | 13 +++++++++++++ thirdparty/CMakeLists.txt | 16 ---------------- 5 files changed, 52 insertions(+), 16 deletions(-) create mode 100755 scripts/thirdparty/cpp-httplib.sh create mode 100755 scripts/thirdparty/fastmcpp.sh create mode 100755 scripts/thirdparty/nlohmann-json.sh diff --git a/scripts/build_thirdparty.sh b/scripts/build_thirdparty.sh index 5fa25480431b..c245b64a260d 100755 --- a/scripts/build_thirdparty.sh +++ b/scripts/build_thirdparty.sh @@ -137,6 +137,13 @@ else # build clip separately CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/clip.sh ${MESHLIB_THIRDPARTY_DIR}/clip + + # Build nlohmann-json separately. It is header-only, this just installs it. It is a dependency of fastmcpp. + CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/nlohmann-json.sh "$MESHLIB_THIRDPARTY_DIR/nlohmann-json" + # Build cpp-httplib separately. It is header-only, this just installs it. It is a dependency of fastmcpp. + CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/cpp-httplib.sh "$MESHLIB_THIRDPARTY_DIR/cpp-httplib" + # Build fastmcpp separately. + CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/fastmcpp.sh "$MESHLIB_THIRDPARTY_DIR/fastmcpp" ./fastmcpp_build "${MESHLIB_THIRDPARTY_ROOT_DIR}" fi popd diff --git a/scripts/thirdparty/cpp-httplib.sh b/scripts/thirdparty/cpp-httplib.sh new file mode 100755 index 000000000000..0614343d8350 --- /dev/null +++ b/scripts/thirdparty/cpp-httplib.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -eo pipefail + +SOURCE_DIR="$1" +BUILD_DIR="${2:-./cpp-httplib_build}" + +CMAKE_OPTIONS="${CMAKE_OPTIONS} \ + -D HTTPLIB_TEST=OFF \ +" + +cmake -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -D CMAKE_C_FLAGS="${CFLAGS}" ${CMAKE_OPTIONS} +cmake --build "${BUILD_DIR}" -j `nproc` +cmake --install "${BUILD_DIR}" diff --git a/scripts/thirdparty/fastmcpp.sh b/scripts/thirdparty/fastmcpp.sh new file mode 100755 index 000000000000..c1df66513e50 --- /dev/null +++ b/scripts/thirdparty/fastmcpp.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -exo pipefail + +SOURCE_DIR="$1" +BUILD_DIR="${2:-./fastmcpp_build}" +INSTALL_DIR="${3:-./fastmcpp_install}" + +CMAKE_OPTIONS="${CMAKE_OPTIONS} \ + -D FASTMCPP_BUILD_TESTS=OFF \ + -D FASTMCPP_BUILD_EXAMPLES=OFF \ + -D FASTMCPP_FETCH_CURL=OFF \ +" + +cmake -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -D CMAKE_C_FLAGS="${CFLAGS}" ${CMAKE_OPTIONS} +cmake --build "${BUILD_DIR}" -j `nproc` + +# Fastmcpp doesn't install any files via CMake. We use the headers directly from the submodule, and just copy the library. +mkdir -p "$INSTALL_DIR/lib" +cp "$BUILD_DIR/libfastmcpp_core.a" "$INSTALL_DIR/lib" diff --git a/scripts/thirdparty/nlohmann-json.sh b/scripts/thirdparty/nlohmann-json.sh new file mode 100755 index 000000000000..2024b1b920df --- /dev/null +++ b/scripts/thirdparty/nlohmann-json.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -eo pipefail + +SOURCE_DIR="$1" +BUILD_DIR="${2:-./nlohmann-json_build}" + +CMAKE_OPTIONS="${CMAKE_OPTIONS} \ + -D BUILD_TESTING=OFF \ +" + +cmake -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -D CMAKE_C_FLAGS="${CFLAGS}" ${CMAKE_OPTIONS} +cmake --build "${BUILD_DIR}" -j `nproc` +cmake --install "${BUILD_DIR}" diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index d06368935de7..e4734cf29af3 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -168,19 +168,3 @@ add_subdirectory(./tinygltf) set(WITH_TESTS OFF CACHE BOOL "") set(WITH_EMBIND OFF CACHE BOOL "") add_subdirectory(./laz-perf) - -IF(NOT MR_EMSCRIPTEN) - # Nlohmann-json is a dependency of fastmcpp. - set(BUILD_TESTING OFF) - add_subdirectory(./nlohmann-json) - - # Cpp-httplib is a dependency of fastmcpp. - set(HTTPLIB_TEST OFF) - add_subdirectory(./cpp-httplib) - - set(FASTMCPP_BUILD_TESTS OFF) - set(FASTMCPP_BUILD_EXAMPLES OFF) - set(FASTMCPP_FETCH_CURL OFF) - # Need a custom subdirectory name here, because it conflicts with the executable named `fastmcpp`. I'm not sure why we install stuff to the build directory. - add_subdirectory(./fastmcpp ./fastmcpp_build) -ENDIF() From db8e76265039c7cf938ac4ce2c5af30a74567c61 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 08:13:23 -0500 Subject: [PATCH 11/45] Try again. --- scripts/thirdparty/fastmcpp.sh | 1 + source/fastmcpp/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/thirdparty/fastmcpp.sh b/scripts/thirdparty/fastmcpp.sh index c1df66513e50..37dddf319adc 100755 --- a/scripts/thirdparty/fastmcpp.sh +++ b/scripts/thirdparty/fastmcpp.sh @@ -5,6 +5,7 @@ SOURCE_DIR="$1" BUILD_DIR="${2:-./fastmcpp_build}" INSTALL_DIR="${3:-./fastmcpp_install}" +# Sync those flags with `source/fastmcpp/CMakeLists.txt`. CMAKE_OPTIONS="${CMAKE_OPTIONS} \ -D FASTMCPP_BUILD_TESTS=OFF \ -D FASTMCPP_BUILD_EXAMPLES=OFF \ diff --git a/source/fastmcpp/CMakeLists.txt b/source/fastmcpp/CMakeLists.txt index 1c05cfbf0d86..2aa0a2e78f0f 100644 --- a/source/fastmcpp/CMakeLists.txt +++ b/source/fastmcpp/CMakeLists.txt @@ -17,4 +17,4 @@ set(FASTMCPP_BUILD_TESTS OFF CACHE BOOL "Build tests") set(FASTMCPP_BUILD_EXAMPLES OFF CACHE BOOL "Build examples") set(FASTMCPP_FETCH_CURL OFF CACHE BOOL "Fetch and build libcurl statically for POST streaming") -add_subdirectory(${MESHINSPECTOR_THIRDPARTY_DIR}/fastmcpp fastmcpp) +add_subdirectory(${MESHLIB_THIRDPARTY_DIR}/fastmcpp fastmcpp) From 13577bab85cc57659bc1ef7817b9c8ed5e930963 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 09:58:52 -0500 Subject: [PATCH 12/45] Try again. --- .github/workflows/build-test-linux-vcpkg.yml | 2 +- .github/workflows/build-test-windows.yml | 2 +- scripts/thirdparty/fastmcpp.sh | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test-linux-vcpkg.yml b/.github/workflows/build-test-linux-vcpkg.yml index 774fe79079e7..da4e51bb88da 100644 --- a/.github/workflows/build-test-linux-vcpkg.yml +++ b/.github/workflows/build-test-linux-vcpkg.yml @@ -115,7 +115,7 @@ jobs: # related issue: https://github.com/actions/checkout/issues/1779 export HOME=${RUNNER_TEMP} git config --global --add safe.directory ${GITHUB_WORKSPACE} - git submodule update --init --recursive --depth 1 thirdparty/imgui thirdparty/mrbind-pybind11 thirdparty/mrbind + git submodule update --init --recursive --depth 1 thirdparty/imgui thirdparty/mrbind-pybind11 thirdparty/mrbind thirdparty/fastmcpp thirdparty/nlohmann-json thirdparty/cpp-httplib - name: Install MRBind if: ${{ inputs.mrbind || inputs.mrbind_c }} diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml index f4b2356353bf..e7ad08a456b3 100644 --- a/.github/workflows/build-test-windows.yml +++ b/.github/workflows/build-test-windows.yml @@ -56,7 +56,7 @@ jobs: - name: Checkout third-party submodules run: | # Download sub-submodules for certain submodules. We don't recurse above in Checkout to improve build performance. See: https://github.com/actions/checkout/issues/1779 - git submodule update --init --recursive --depth 1 thirdparty/mrbind + git submodule update --init --recursive --depth 1 thirdparty/mrbind thirdparty/fastmcpp thirdparty/nlohmann-json thirdparty/cpp-httplib - name: Get AWS instance type uses: ./.github/actions/get-aws-instance-type diff --git a/scripts/thirdparty/fastmcpp.sh b/scripts/thirdparty/fastmcpp.sh index 37dddf319adc..c3e17317dd81 100755 --- a/scripts/thirdparty/fastmcpp.sh +++ b/scripts/thirdparty/fastmcpp.sh @@ -17,4 +17,5 @@ cmake --build "${BUILD_DIR}" -j `nproc` # Fastmcpp doesn't install any files via CMake. We use the headers directly from the submodule, and just copy the library. mkdir -p "$INSTALL_DIR/lib" +cp -R "$SOURCE_DIR/include/fastmcpp" "$INSTALL_DIR/include" cp "$BUILD_DIR/libfastmcpp_core.a" "$INSTALL_DIR/lib" From f34dcdebe0b4e9ad83957245b3cae9aab699f541 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 11:11:18 -0500 Subject: [PATCH 13/45] Try again. --- scripts/thirdparty/fastmcpp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/thirdparty/fastmcpp.sh b/scripts/thirdparty/fastmcpp.sh index c3e17317dd81..474e528b30a7 100755 --- a/scripts/thirdparty/fastmcpp.sh +++ b/scripts/thirdparty/fastmcpp.sh @@ -17,5 +17,5 @@ cmake --build "${BUILD_DIR}" -j `nproc` # Fastmcpp doesn't install any files via CMake. We use the headers directly from the submodule, and just copy the library. mkdir -p "$INSTALL_DIR/lib" -cp -R "$SOURCE_DIR/include/fastmcpp" "$INSTALL_DIR/include" +cp -R "$SOURCE_DIR"/include/fastmcpp* "$INSTALL_DIR/include" cp "$BUILD_DIR/libfastmcpp_core.a" "$INSTALL_DIR/lib" From 826e9b84543a4d0601931770836a30ad4040ed50 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 12:21:13 -0500 Subject: [PATCH 14/45] Try again. --- scripts/thirdparty/fastmcpp.sh | 1 + source/fastmcpp/fastmcpp.vcxproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/thirdparty/fastmcpp.sh b/scripts/thirdparty/fastmcpp.sh index 474e528b30a7..59eef55fb4a3 100755 --- a/scripts/thirdparty/fastmcpp.sh +++ b/scripts/thirdparty/fastmcpp.sh @@ -10,6 +10,7 @@ CMAKE_OPTIONS="${CMAKE_OPTIONS} \ -D FASTMCPP_BUILD_TESTS=OFF \ -D FASTMCPP_BUILD_EXAMPLES=OFF \ -D FASTMCPP_FETCH_CURL=OFF \ + -D CMAKE_CXX_FLAGS=-fPIC \ " cmake -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -D CMAKE_C_FLAGS="${CFLAGS}" ${CMAKE_OPTIONS} diff --git a/source/fastmcpp/fastmcpp.vcxproj b/source/fastmcpp/fastmcpp.vcxproj index 9d55827c6f0b..1af31057cc18 100644 --- a/source/fastmcpp/fastmcpp.vcxproj +++ b/source/fastmcpp/fastmcpp.vcxproj @@ -134,6 +134,7 @@ Win32Proj MRInterprocess + StaticLibrary @@ -146,7 +147,6 @@ false Unicode - From b0a19352b4deec5214a36dc834ab8a8ea4135449 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Mon, 13 Apr 2026 23:31:01 -0500 Subject: [PATCH 15/45] Try again. --- source/fastmcpp/fastmcpp.vcxproj | 12 ++++-------- thirdparty/fastmcpp | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/source/fastmcpp/fastmcpp.vcxproj b/source/fastmcpp/fastmcpp.vcxproj index 1af31057cc18..d744be09f477 100644 --- a/source/fastmcpp/fastmcpp.vcxproj +++ b/source/fastmcpp/fastmcpp.vcxproj @@ -132,9 +132,8 @@ 15.0 {7853AEC9-A364-4587-89AE-FAA9A463E6ED} Win32Proj - MRInterprocess + fastmcpp - StaticLibrary @@ -147,6 +146,7 @@ false Unicode + @@ -166,7 +166,7 @@ EnableAllWarnings Disabled true - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + _DEBUG;%(PreprocessorDefinitions) true true %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include @@ -190,7 +190,7 @@ true true true - NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + NDEBUG;%(PreprocessorDefinitions) true true /bigobj %(AdditionalOptions) @@ -203,10 +203,6 @@ true true - - - - diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index 375ec467dfd1..c8743b9a9026 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit 375ec467dfd1aa5babcec20020ac11f41405a0d4 +Subproject commit c8743b9a9026365880d1984ca10809eaa60cfadd From ac7c5d2b8537a6655868cdb9caf6c628b6f8c117 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Tue, 14 Apr 2026 02:48:36 -0500 Subject: [PATCH 16/45] Try again. --- source/fastmcpp/fastmcpp.vcxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/fastmcpp/fastmcpp.vcxproj b/source/fastmcpp/fastmcpp.vcxproj index d744be09f477..8da95a913fc6 100644 --- a/source/fastmcpp/fastmcpp.vcxproj +++ b/source/fastmcpp/fastmcpp.vcxproj @@ -170,7 +170,7 @@ true true %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include - 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;%(DisableSpecificWarnings) + 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) Console @@ -195,7 +195,7 @@ true /bigobj %(AdditionalOptions) %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include - 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;%(DisableSpecificWarnings) + 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) Console From 24d3bda6e3e591ced19135c06f8df4f4c49dc0a3 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Tue, 14 Apr 2026 04:18:28 -0500 Subject: [PATCH 17/45] Try again. --- cmake/Modules/CompilerOptions.cmake | 8 +++++++- source/fastmcpp/fastmcpp.vcxproj | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cmake/Modules/CompilerOptions.cmake b/cmake/Modules/CompilerOptions.cmake index 61e077fe452e..dbee27d76435 100644 --- a/cmake/Modules/CompilerOptions.cmake +++ b/cmake/Modules/CompilerOptions.cmake @@ -27,12 +27,18 @@ ELSE() # if APPLE IF( CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 17 ) # ethier AppleClang or Clang set(MESHLIB_COMMON_C_CXX_FLAGS "${MESHLIB_COMMON_C_CXX_FLAGS} -fno-assume-unique-vtables") ENDIF() + + # This somehow works around `Undefined symbols for architecture arm64: "std::exception_ptr::__from_native_exception_pointer(void*)"`. + # See: https://github.com/llvm/llvm-project/issues/86077 + IF(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 18 AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -lc++abi") + ENDIF() ENDIF() # Warnings and misc compiler settings. IF(MSVC) # C++-specific flags. - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DImDrawIdx=unsigned /D_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING /D_SILENCE_CXX20_OLD_SHARED_PTR_ATOMIC_SUPPORT_DEPRECATION_WARNING /D_SILENCE_CXX23_ALIGNED_STORAGE_DEPRECATION_WARNING /D_SILENCE_CXX23_DENORM_DEPRECATION_WARNING /D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DImDrawIdx=unsigned /D_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING /D_SILENCE_CXX20_OLD_SHARED_PTR_ATOMIC_SUPPORT_DEPRECATION_WARNING /D_SILENCE_CXX23_ALIGNED_STORAGE_DEPRECATION_WARNING /D_SILENCE_CXX23_DENORM_DEPRECATION_WARNING /D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR") # Common C/C++ flags: diff --git a/source/fastmcpp/fastmcpp.vcxproj b/source/fastmcpp/fastmcpp.vcxproj index 8da95a913fc6..650d843419d8 100644 --- a/source/fastmcpp/fastmcpp.vcxproj +++ b/source/fastmcpp/fastmcpp.vcxproj @@ -170,7 +170,7 @@ true true %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include - 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) + 4099;4100;4242;4244;4355;4456;4458;4464;4505;4702;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) Console @@ -195,7 +195,7 @@ true /bigobj %(AdditionalOptions) %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include - 4099;4100;4242;4244;4355;4456;4458;4464;4505;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) + 4099;4100;4242;4244;4355;4456;4458;4464;4505;4702;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) Console From e246fccbfeb8e6d0aba2048669589ca2d08fad71 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Tue, 14 Apr 2026 04:30:25 -0500 Subject: [PATCH 18/45] Try again. --- cmake/Modules/CompilerOptions.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/Modules/CompilerOptions.cmake b/cmake/Modules/CompilerOptions.cmake index dbee27d76435..a77fa8d20288 100644 --- a/cmake/Modules/CompilerOptions.cmake +++ b/cmake/Modules/CompilerOptions.cmake @@ -32,6 +32,7 @@ ELSE() # if APPLE # See: https://github.com/llvm/llvm-project/issues/86077 IF(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 18 AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -lc++abi") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -lc++abi") ENDIF() ENDIF() From 1331089f55feca5fbc4a0bad83876114899cb483 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Tue, 14 Apr 2026 06:17:20 -0500 Subject: [PATCH 19/45] Try again. --- cmake/Modules/CompilerOptions.cmake | 7 ------- source/MRPch/MRPch.h | 9 +++++++++ source/MRViewer/MRMcp.cpp | 22 +++++++++++++--------- thirdparty/fastmcpp | 2 +- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/cmake/Modules/CompilerOptions.cmake b/cmake/Modules/CompilerOptions.cmake index a77fa8d20288..1df0ee1168aa 100644 --- a/cmake/Modules/CompilerOptions.cmake +++ b/cmake/Modules/CompilerOptions.cmake @@ -27,13 +27,6 @@ ELSE() # if APPLE IF( CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 17 ) # ethier AppleClang or Clang set(MESHLIB_COMMON_C_CXX_FLAGS "${MESHLIB_COMMON_C_CXX_FLAGS} -fno-assume-unique-vtables") ENDIF() - - # This somehow works around `Undefined symbols for architecture arm64: "std::exception_ptr::__from_native_exception_pointer(void*)"`. - # See: https://github.com/llvm/llvm-project/issues/86077 - IF(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 18 AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -lc++abi") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -lc++abi") - ENDIF() ENDIF() # Warnings and misc compiler settings. diff --git a/source/MRPch/MRPch.h b/source/MRPch/MRPch.h index 794d98476302..2e75f7a26a1a 100644 --- a/source/MRPch/MRPch.h +++ b/source/MRPch/MRPch.h @@ -1,5 +1,14 @@ #pragma once +// Work around Clang quirk: https://github.com/llvm/llvm-project/issues/86077 +// This quirk causes issues for Fastmcpp on Mac Arm. +// This must be included before `` to work correctly, so effectively before any standard library headers. +#if defined( __APPLE__ ) && defined( __arm64__ ) +#include <__availability> +#undef _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION +#define _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION 0 +#endif + #pragma warning(push) #pragma warning(disable: 4820) //#pragma warning: N bytes padding added after data member diff --git a/source/MRViewer/MRMcp.cpp b/source/MRViewer/MRMcp.cpp index f8d8215c7016..d219ead1ff09 100644 --- a/source/MRViewer/MRMcp.cpp +++ b/source/MRViewer/MRMcp.cpp @@ -1,12 +1,3 @@ -#include "MRMcp.h" - -#if MR_ENABLE_MCP_SERVER -#include "MRMesh/MRSystem.h" -#include "MRPch/MRSpdlog.h" -#include "MRViewer/MRCommandLoop.h" -#include "MRViewer/MRUITestEngineControl.h" -#include "MRViewer/MRViewer.h" - #undef _t // Our translation macro interefers with Fastmcpp. #if defined( __GNUC__ ) @@ -18,6 +9,8 @@ #pragma warning( disable: 4355 ) // 'this': used in base member initializer list #endif +// This must be included before any standard library headers, because of the macro shenanigans we added to that header. +// Those are duplicated into our PCH, so that shouldn't interfere. #include #include @@ -31,6 +24,17 @@ #endif +#include "MRMcp.h" + +#if MR_ENABLE_MCP_SERVER +#include "MRMesh/MRSystem.h" +#include "MRPch/MRSpdlog.h" +#include "MRViewer/MRCommandLoop.h" +#include "MRViewer/MRUITestEngineControl.h" +#include "MRViewer/MRViewer.h" + + + /* HOW TO TEST MCP: Use "MCP Inspector". Run it with `npx @modelcontextprotocol/inspector`, where `npx` is installed as a part of Node.JS. diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index c8743b9a9026..c0d01d468802 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit c8743b9a9026365880d1984ca10809eaa60cfadd +Subproject commit c0d01d46880270102c6c1dfc27001a54979c9df7 From 8adf23e41c6cadd7eb909b47162ba43c4119bb6b Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Tue, 14 Apr 2026 07:16:17 -0500 Subject: [PATCH 20/45] Try again. --- source/MRViewer/MRMcp.cpp | 11 ++++++++++- source/MRViewer/MRMcp.h | 10 ---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/source/MRViewer/MRMcp.cpp b/source/MRViewer/MRMcp.cpp index d219ead1ff09..7f782a84dd3a 100644 --- a/source/MRViewer/MRMcp.cpp +++ b/source/MRViewer/MRMcp.cpp @@ -1,3 +1,13 @@ +#ifndef MR_ENABLE_MCP_SERVER +# ifdef __EMSCRIPTEN__ +# define MR_ENABLE_MCP_SERVER 0 +# else +# define MR_ENABLE_MCP_SERVER 1 +# endif +#endif + +#if MR_ENABLE_MCP_SERVER + #undef _t // Our translation macro interefers with Fastmcpp. #if defined( __GNUC__ ) @@ -26,7 +36,6 @@ #include "MRMcp.h" -#if MR_ENABLE_MCP_SERVER #include "MRMesh/MRSystem.h" #include "MRPch/MRSpdlog.h" #include "MRViewer/MRCommandLoop.h" diff --git a/source/MRViewer/MRMcp.h b/source/MRViewer/MRMcp.h index a11d999bca27..1c484cf4e14f 100644 --- a/source/MRViewer/MRMcp.h +++ b/source/MRViewer/MRMcp.h @@ -2,18 +2,8 @@ #include "exports.h" -#include "MRViewer/MRRibbonMenuItem.h" - #include -#ifndef MR_ENABLE_MCP_SERVER -# ifdef __EMSCRIPTEN__ -# define MR_ENABLE_MCP_SERVER 0 -# else -# define MR_ENABLE_MCP_SERVER 1 -# endif -#endif - namespace MR { From 11bb94eb9f51912ba9b32d05e33c232e96e452d8 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Tue, 14 Apr 2026 10:06:31 -0500 Subject: [PATCH 21/45] Try again. --- thirdparty/fastmcpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index c0d01d468802..3b6b5f8a5d1c 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit c0d01d46880270102c6c1dfc27001a54979c9df7 +Subproject commit 3b6b5f8a5d1c332cd61babf1bc49048fe2af9e5a From dede838575cddc9082c1652b442848a652d14468 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Tue, 14 Apr 2026 10:18:44 -0500 Subject: [PATCH 22/45] Forgot to change the header in one place. --- source/MRPch/MRPch.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/MRPch/MRPch.h b/source/MRPch/MRPch.h index 2e75f7a26a1a..da3d3b1459dc 100644 --- a/source/MRPch/MRPch.h +++ b/source/MRPch/MRPch.h @@ -4,7 +4,7 @@ // This quirk causes issues for Fastmcpp on Mac Arm. // This must be included before `` to work correctly, so effectively before any standard library headers. #if defined( __APPLE__ ) && defined( __arm64__ ) -#include <__availability> +#include #undef _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION #define _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION 0 #endif From 03b99b2df5d2d4df00a411a0b2cd8137261c85ee Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Wed, 15 Apr 2026 10:36:06 -0500 Subject: [PATCH 23/45] Address some PR comments. --- CMakeLists.txt | 11 + scripts/thirdparty/cpp-httplib.sh | 4 + scripts/thirdparty/fastmcpp.sh | 7 +- scripts/thirdparty/nlohmann-json.sh | 2 +- source/MRMcp/CMakeLists.txt | 55 ++++ source/MRMcp/MRMcp.cpp | 171 +++++++++++ source/MRMcp/MRMcp.h | 146 +++++++++ source/MRMcp/MRMcp.vcxproj | 107 +++++++ source/MRMcp/exports.h | 18 ++ source/MRMesh/MRSystem.cpp | 5 + source/MRMesh/MRSystem.h | 3 + source/MRViewer/CMakeLists.txt | 19 +- source/MRViewer/MRMcp.cpp | 440 ---------------------------- source/MRViewer/MRMcp.h | 38 --- source/MRViewer/MRSetupMcp.cpp | 15 + source/MRViewer/MRViewer.vcxproj | 7 +- source/MRViewer/MRViewerMcp.cpp | 178 +++++++++++ source/MeshLib.sln | 7 + thirdparty/fastmcpp | 2 +- 19 files changed, 729 insertions(+), 506 deletions(-) create mode 100644 source/MRMcp/CMakeLists.txt create mode 100644 source/MRMcp/MRMcp.cpp create mode 100644 source/MRMcp/MRMcp.h create mode 100644 source/MRMcp/MRMcp.vcxproj create mode 100644 source/MRMcp/exports.h delete mode 100644 source/MRViewer/MRMcp.cpp delete mode 100644 source/MRViewer/MRMcp.h create mode 100644 source/MRViewer/MRSetupMcp.cpp create mode 100644 source/MRViewer/MRViewerMcp.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 98a727dea245..48a6f9ff21b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ option(MESHLIB_BUILD_MESHCONV "Build meshconv utility" ON) option(MESHLIB_BUILD_SYMBOLMESH "Build symbol-to-mesh library" ON) option(MESHLIB_BUILD_VOXELS "Build voxels library" ON) option(MESHLIB_BUILD_EXTRA_IO_FORMATS "Build extra IO format support library" ON) +option(MESHLIB_BUILD_MCP "Enable MCP server" ON) option(MESHLIB_BUILD_GENERATED_C_BINDINGS "Build C bindings (assuming they are already generated)" OFF) option(MESHLIB_BUILD_MRCUDA "Build MRCuda library" ON) @@ -293,6 +294,16 @@ IF(NOT MR_EMSCRIPTEN AND NOT APPLE) ENDIF() ENDIF() +IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) + # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. + IF(MESHLIB_USE_VCPKG) + add_subdirectory(../fastmcpp fastmcpp) + ELSE() + find_package(fastmcpp REQUIRED) + ENDIF() + add_subdirectory(${PROJECT_SOURCE_DIR}/MRMcp ./MRMcp) +ENDIF() + IF(BUILD_TESTING) enable_testing() add_subdirectory(${PROJECT_SOURCE_DIR}/MRTest ./MRTest) diff --git a/scripts/thirdparty/cpp-httplib.sh b/scripts/thirdparty/cpp-httplib.sh index 0614343d8350..918b8a36d005 100755 --- a/scripts/thirdparty/cpp-httplib.sh +++ b/scripts/thirdparty/cpp-httplib.sh @@ -6,6 +6,10 @@ BUILD_DIR="${2:-./cpp-httplib_build}" CMAKE_OPTIONS="${CMAKE_OPTIONS} \ -D HTTPLIB_TEST=OFF \ + -D HTTPLIB_USE_BROTLI_IF_AVAILABLE=OFF \ + -D HTTPLIB_USE_OPENSSL_IF_AVAILABLE=OFF \ + -D HTTPLIB_USE_ZLIB_IF_AVAILABLE=OFF \ + -D HTTPLIB_USE_ZSTD_IF_AVAILABLE=OFF \ " cmake -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -D CMAKE_C_FLAGS="${CFLAGS}" ${CMAKE_OPTIONS} diff --git a/scripts/thirdparty/fastmcpp.sh b/scripts/thirdparty/fastmcpp.sh index 59eef55fb4a3..7b9e241a1c11 100755 --- a/scripts/thirdparty/fastmcpp.sh +++ b/scripts/thirdparty/fastmcpp.sh @@ -3,7 +3,6 @@ set -exo pipefail SOURCE_DIR="$1" BUILD_DIR="${2:-./fastmcpp_build}" -INSTALL_DIR="${3:-./fastmcpp_install}" # Sync those flags with `source/fastmcpp/CMakeLists.txt`. CMAKE_OPTIONS="${CMAKE_OPTIONS} \ @@ -15,8 +14,4 @@ CMAKE_OPTIONS="${CMAKE_OPTIONS} \ cmake -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -D CMAKE_C_FLAGS="${CFLAGS}" ${CMAKE_OPTIONS} cmake --build "${BUILD_DIR}" -j `nproc` - -# Fastmcpp doesn't install any files via CMake. We use the headers directly from the submodule, and just copy the library. -mkdir -p "$INSTALL_DIR/lib" -cp -R "$SOURCE_DIR"/include/fastmcpp* "$INSTALL_DIR/include" -cp "$BUILD_DIR/libfastmcpp_core.a" "$INSTALL_DIR/lib" +cmake --install "${BUILD_DIR}" diff --git a/scripts/thirdparty/nlohmann-json.sh b/scripts/thirdparty/nlohmann-json.sh index 2024b1b920df..6141c0ef51c5 100755 --- a/scripts/thirdparty/nlohmann-json.sh +++ b/scripts/thirdparty/nlohmann-json.sh @@ -5,7 +5,7 @@ SOURCE_DIR="$1" BUILD_DIR="${2:-./nlohmann-json_build}" CMAKE_OPTIONS="${CMAKE_OPTIONS} \ - -D BUILD_TESTING=OFF \ + -D JSON_BuildTests=OFF \ " cmake -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -D CMAKE_C_FLAGS="${CFLAGS}" ${CMAKE_OPTIONS} diff --git a/source/MRMcp/CMakeLists.txt b/source/MRMcp/CMakeLists.txt new file mode 100644 index 000000000000..44b4deea004d --- /dev/null +++ b/source/MRMcp/CMakeLists.txt @@ -0,0 +1,55 @@ +project(MRMcp CXX) + +file(GLOB HEADERS "*.h" "*.ipp") +file(GLOB SOURCES "*.cpp") + +add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS}) + +target_link_libraries(${PROJECT_NAME} + PUBLIC + MRMesh + fastmcpp::fastmcpp_core +) + +IF(MR_PCH) + target_precompile_headers(${PROJECT_NAME} REUSE_FROM MRPch) +ENDIF() + +# Fastmcpp stuff. +option(MESHLIB_BUILD_MCP "Enable MCP server" ON) +IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) + target_link_libraries(${PROJECT_NAME} PRIVATE fastmcpp_core) + + target_include_directories(${PROJECT_NAME} PRIVATE + ${MESHLIB_THIRDPARTY_DIR}/fastmcpp/include + ${MESHLIB_THIRDPARTY_DIR}/cpp-httplib + ${MESHLIB_THIRDPARTY_DIR}/nlohmann-json/include + ) +ELSE() + target_compile_definitions(${PROJECT_NAME} PRIVATE MR_ENABLE_MCP_SERVER=0) +ENDIF() + +install( + TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME} + LIBRARY DESTINATION "${MR_MAIN_LIB_DIR}" + ARCHIVE DESTINATION "${MR_MAIN_LIB_DIR}" + RUNTIME DESTINATION "${MR_BIN_DIR}" +) + +install( + FILES ${HEADERS} + DESTINATION "${MR_INCLUDE_DIR}/${PROJECT_NAME}" +) + +install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}Config.cmake + DESTINATION ${MR_CONFIG_DIR} +) + +install( + EXPORT ${PROJECT_NAME} + FILE ${PROJECT_NAME}Targets.cmake + NAMESPACE MeshLib:: + DESTINATION ${MR_CONFIG_DIR} +) diff --git a/source/MRMcp/MRMcp.cpp b/source/MRMcp/MRMcp.cpp new file mode 100644 index 000000000000..f4d7de9db175 --- /dev/null +++ b/source/MRMcp/MRMcp.cpp @@ -0,0 +1,171 @@ +// Must not include any standard headers + +#undef _t // Our translation macro interefers with Fastmcpp. + +#if defined( __GNUC__ ) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#elif defined( _MSC_VER ) +#pragma warning( push ) +#pragma warning( disable: 4100 ) // unreferenced formal parameter +#pragma warning( disable: 4355 ) // 'this': used in base member initializer list +#endif + +// This must be included before any standard library headers, because of the macro shenanigans we added to that header. +// Those are duplicated into our PCH, so that shouldn't interfere. +#include +#include + +#if defined( __GNUC__ ) +#pragma GCC diagnostic pop +#elif defined( _MSC_VER ) +#pragma warning( pop ) +#endif + +#include "MRMcp.h" + +#include "MRMesh/MRSystem.h" +#include "MRPch/MRSpdlog.h" +#include "MRViewer/MRCommandLoop.h" +#include "MRViewer/MRUITestEngineControl.h" +#include "MRViewer/MRViewer.h" + +#include + + +/* HOW TO TEST MCP: + +Use "MCP Inspector". Run it with `npx @modelcontextprotocol/inspector`, where `npx` is installed as a part of Node.JS. +Set: + Transport Type = SSE + URL = http://localhost:8080/sse + Connection Type = Via Proxy (Doesn't work for me without proxy now when we're using the Fastmcpp library, but did work with another library; not sure why.) + +Press `Connect`. +Press `List Tools` (if grayed out, do `Clear` first). +Click on your tool. +On the right panel, set parameters. + For some parameter types, it helps to press `Switch to JSON` on the right, then type them as JSON. +Press `Run Tool`. + Note, you might need to press this twice. In some cases, the first press passes stale/empty parameters. +Then check for validation errors, below this button. + +If your output doesn't match the schema you specified, paste both the output and the schema (using the `Copy` button in the top-right corner of the code blocks; that copies JSON properly, unlike Ctrl+C in this case) + into a schema validator, e.g. https://www.jsonschemavalidator.net/ + +*/ + + +namespace MR::Mcp +{ + +struct Server::State +{ + Params params; + fastmcpp::tools::ToolManager toolManager; // This has to be persistent, or `fastmcpp::mcp::make_mcp_handler()` dangles it. + std::unordered_map toolDescs; // No idea why this is not a part of `toolManager`. + std::optional server; + + void createServer() + { + assert( !server ); + server.emplace( fastmcpp::mcp::make_mcp_handler( params.name, params.version, toolManager, toolDescs ), params.address, params.port ); + } +}; + +Server::Params::Params() + : name( getProductName() ), + version( GetMRVersionString() ) +{} + +Server::Server() = default; +Server::Server( Server&& ) = default; +Server& Server::operator=( Server&& ) = default; +Server::~Server() = default; + +bool Server::addTool( std::string id, std::string name, std::string desc, Schema::Base inputSchema, Schema::Base outputSchema, ToolFunc func ) +{ + if ( !state_ ) + { + state_ = std::make_unique(); + } + else if ( state_->server ) + { + assert( false && "`MR::Mcp::Server::addTool()`: Called too late, the server is already initialized." ); + return false; + } + + // Why is managing the descriptions not automated by the library? + if ( !state_->toolDescs.try_emplace( id, desc ).second ) + { + assert( false && "`MR::Mcp::Server::addTool()`: Duplicate tool id." ); + return false; + } + + state_->toolManager.register_tool( fastmcpp::tools::Tool( id, std::move( inputSchema ).asJson(), std::move( outputSchema ).asJson(), func, name, desc, {} ) ); + return true; +} + +Server::Params Server::getParams() const +{ + if ( state_ ) + return state_->params; + else + return {}; +} + +void Server::setParams( Server::Params params ) +{ + const bool serverExisted = state_ && bool( state_->server ); + const bool serverWasRunning = serverExisted && isRunning(); + if ( serverWasRunning ) + setRunning( false ); + + state_->server.reset(); + state_->params = std::move( params ); + + if ( serverExisted ) + state_->createServer(); + if ( serverWasRunning ) + setRunning( true ); +} + +bool Server::isRunning() const +{ + return state_ && state_->server && state_->server->running(); +} + +bool Server::setRunning( bool enable ) +{ + if ( enable ) + { + if ( !state_ ) + state_ = std::make_unique(); + if ( !state_->server ) + state_->createServer(); + + bool ok = state_->server->start(); + if ( ok ) + spdlog::info( "MCP server started on port {}", getParams().port ); + else + spdlog::error( "MCP server failed to start on port {}", getParams().port ); + return ok; + } + else + { + if ( state_ && state_->server ) + { + state_->server->stop(); + spdlog::info( "MCP server stopped" ); + } + return true; + } +} + +Server& getDefaultServer() +{ + static Server ret; + return ret; +} + +} // namespace MR diff --git a/source/MRMcp/MRMcp.h b/source/MRMcp/MRMcp.h new file mode 100644 index 000000000000..9673833a0fae --- /dev/null +++ b/source/MRMcp/MRMcp.h @@ -0,0 +1,146 @@ +#pragma once + +#include "exports.h" + +#include + +#include +#include + +namespace MR::Mcp +{ + +// This is used to build json schemas. +namespace Schema +{ + struct Base + { + protected: + nlohmann::json json; + Base( nlohmann::json json ) : json( std::move( json ) ) {} + + public: + [[nodiscard]] const nlohmann::json& asJson() const & { return json; } + [[nodiscard]] nlohmann::json&& asJson() && { return std::move( json ); } + }; + + struct Empty : Base + { + Empty() : Base( {} ) {} + }; + + struct Number : Base + { + Number() + : Base( nlohmann::json::object( { + { "type", "number" }, + } ) ) + {} + }; + + struct String : Base + { + String() + : Base( nlohmann::json::object( { + { "type", "string" }, + } ) ) + {} + }; + + struct Array : Base + { + Array( Base elemSchema ) + : Base( nlohmann::json::object( { + { "type", "array" }, + { "items", std::move( elemSchema ).asJson() }, + } ) ) + {} + }; + + struct Object : Base + { + Object() + : Base( nlohmann::json::object( { + { "type", "object" }, + { "properties", nlohmann::json::object() }, + { "required", nlohmann::json::array() }, + } ) ) + {} + + // Add required member. Returns a reference to `*this`. + Object &addMember( std::string name, Base schema ) & + { + json.at("required").push_back( name ); + addMemberOpt( std::move( name ), std::move( schema ) ); + return *this; + } + // Add optional member. Returns a reference to `*this`. + Object &addMemberOpt( std::string name, Base schema ) & + { + json.at( "properties" ).push_back( nlohmann::json::object_t::value_type( std::move( name ), std::move( schema ).asJson() ) ); + return *this; + } + + // Add required member. Returns a reference to `*this`. + [[nodiscard]] Object&& addMember( std::string name, Base schema ) && + { + addMember( std::move( name ), std::move( schema ) ); + return std::move( *this ); + } + // Add optional member. Returns a reference to `*this`. + [[nodiscard]] Object&& addMemberOpt( std::string name, Base schema ) && + { + addMemberOpt( std::move( name ), std::move( schema ) ); + return std::move( *this ); + } + }; +} // namespace Schema + +class Server +{ + struct State; + + // This is null until either `setParams()` or `setRunning(true)` is called for the first time. + std::unique_ptr state_; + +public: + struct Params + { + std::string address = "127.0.0.1"; // You don't need to change this, unless you want to accept connections from the outside world. + int port = 7887; + std::string name; // A default string is set in the constructor. + std::string version; // A default string is set in the constructor. + + friend bool operator==( const Params&, const Params& ) = default; + + MRMCP_API Params(); + }; + + MRMCP_API Server(); + MRMCP_API Server( Server&& ); + MRMCP_API Server& operator=( Server&& ); + MRMCP_API ~Server(); + + // Those functions are allowed to throw, that's how you report errors to the MCP. + using ToolFunc = std::function; + + // Registers a new tool. + // Fails if the tool with this `id` already exists. + // Must be called early, before `setRunning(true)` is called for the first time, otherwise fails. + // Returns true on success. Asserts when returning false, so you don't have to check the return value. + MRMCP_API bool addTool( std::string id, std::string name, std::string desc, Schema::Base inputSchema, Schema::Base outputSchema, ToolFunc func ); + + [[nodiscard]] MRMCP_API Params getParams() const; + + // This restarts the server if necessary. + MRMCP_API void setParams( Params params ); + + [[nodiscard]] MRMCP_API bool isRunning() const; + // Returns true on success, including if the server is already running and you're trying to start it again. + // Stopping always returns true. + MRMCP_API bool setRunning( bool enable ); +}; + +[[nodiscard]] MRMCP_API Server& getDefaultServer(); + +} // namespace MR diff --git a/source/MRMcp/MRMcp.vcxproj b/source/MRMcp/MRMcp.vcxproj new file mode 100644 index 000000000000..b6686e94bb58 --- /dev/null +++ b/source/MRMcp/MRMcp.vcxproj @@ -0,0 +1,107 @@ + + + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + {7853aec9-a364-4587-89ae-faa9a463e6ed} + + + {c7780500-ca0e-4f5f-8423-d7ab06078b14} + + + + 15.0 + {C8250F26-E01D-4A63-98CD-68069D818080} + Win32Proj + MRMcp + + + + StaticLibrary + true + Unicode + + + StaticLibrary + false + false + Unicode + + + + + + + + + + + + + + + + + + NotUsing + EnableAllWarnings + Disabled + true + MRMcp_EXPORTS;_DEBUG;%(PreprocessorDefinitions) + true + true + %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include + 4099;4100;4242;4244;4355;4456;4458;4464;4505;4702;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) + + + Console + true + %(AdditionalDependencies) + + + + + + + + + NotUsing + EnableAllWarnings + MaxSpeed + true + true + true + MRMcp_EXPORTS;NDEBUG;%(PreprocessorDefinitions) + true + true + /bigobj %(AdditionalOptions) + %(AdditionalIncludeDirectories);..\..\thirdparty\fastmcpp\include;..\..\thirdparty\cpp-httplib;..\..\thirdparty\nlohmann-json\include + 4099;4100;4242;4244;4355;4456;4458;4464;4505;4702;5204;5220;5233;5245;5267;%(DisableSpecificWarnings) + + + Console + true + true + true + + + + + + diff --git a/source/MRMcp/exports.h b/source/MRMcp/exports.h new file mode 100644 index 000000000000..cd31c682c5a6 --- /dev/null +++ b/source/MRMcp/exports.h @@ -0,0 +1,18 @@ +#pragma once + +// see explanation in MRMesh/MRMeshFwd.h +#ifdef _WIN32 +# ifdef MRMcp_EXPORTS +# define MRMCP_API __declspec(dllexport) +# else +# define MRMCP_API __declspec(dllimport) +# endif +# define MRMCP_CLASS +#else +# define MRMCP_API __attribute__((visibility("default"))) +# ifdef __clang__ +# define MRMCP_CLASS __attribute__((type_visibility("default"))) +# else +# define MRMCP_CLASS __attribute__((visibility("default"))) +# endif +#endif diff --git a/source/MRMesh/MRSystem.cpp b/source/MRMesh/MRSystem.cpp index d7189e597fd2..6ed2d726179f 100644 --- a/source/MRMesh/MRSystem.cpp +++ b/source/MRMesh/MRSystem.cpp @@ -94,6 +94,11 @@ void removeOldLogs( const std::filesystem::path& dir, int hours = 24 ) namespace MR { +std::string getProductName() +{ + return MR_PROJECT_NAME; +} + void SetCurrentThreadName( const char * name ) { #ifdef _MSC_VER diff --git a/source/MRMesh/MRSystem.h b/source/MRMesh/MRSystem.h index 1ecb840327e1..a0eb80ce700c 100644 --- a/source/MRMesh/MRSystem.h +++ b/source/MRMesh/MRSystem.h @@ -8,6 +8,9 @@ namespace MR { +// The name of the current application. +[[nodiscard]] MRMESH_API MR_BIND_IGNORE std::string getProductName(); + // sets debug name for the current thread MRMESH_API void SetCurrentThreadName( const char * name ); diff --git a/source/MRViewer/CMakeLists.txt b/source/MRViewer/CMakeLists.txt index ae8cadd4b7ba..eeec1aa916f8 100644 --- a/source/MRViewer/CMakeLists.txt +++ b/source/MRViewer/CMakeLists.txt @@ -111,23 +111,10 @@ IF(MR_PCH) target_precompile_headers(${PROJECT_NAME} REUSE_FROM MRPch) ENDIF() -# Fastmcpp stuff. -option(MESHLIB_BUILD_MCP "Enable MCP server" ON) -IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) - # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. - IF(MESHLIB_USE_VCPKG) - add_subdirectory(../fastmcpp fastmcpp) - ENDIF() - - target_link_libraries(${PROJECT_NAME} PRIVATE fastmcpp_core) - - target_include_directories(${PROJECT_NAME} PRIVATE - ${MESHLIB_THIRDPARTY_DIR}/fastmcpp/include - ${MESHLIB_THIRDPARTY_DIR}/cpp-httplib - ${MESHLIB_THIRDPARTY_DIR}/nlohmann-json/include - ) +IF(MESHLIB_BUILD_MCP) + target_link_libraries(${PROJECT_NAME} PRIVATE MRMcp) ELSE() - target_compile_definitions(${PROJECT_NAME} PRIVATE MR_ENABLE_MCP_SERVER=0) + target_compile_definitions(${PROJECT_NAME} PRIVATE MESHLIB_NO_MCP) ENDIF() file(GLOB JSONS "*.json") diff --git a/source/MRViewer/MRMcp.cpp b/source/MRViewer/MRMcp.cpp deleted file mode 100644 index 7f782a84dd3a..000000000000 --- a/source/MRViewer/MRMcp.cpp +++ /dev/null @@ -1,440 +0,0 @@ -#ifndef MR_ENABLE_MCP_SERVER -# ifdef __EMSCRIPTEN__ -# define MR_ENABLE_MCP_SERVER 0 -# else -# define MR_ENABLE_MCP_SERVER 1 -# endif -#endif - -#if MR_ENABLE_MCP_SERVER - -#undef _t // Our translation macro interefers with Fastmcpp. - -#if defined( __GNUC__ ) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -#elif defined( _MSC_VER ) -#pragma warning( push ) -#pragma warning( disable: 4100 ) // unreferenced formal parameter -#pragma warning( disable: 4355 ) // 'this': used in base member initializer list -#endif - -// This must be included before any standard library headers, because of the macro shenanigans we added to that header. -// Those are duplicated into our PCH, so that shouldn't interfere. -#include -#include - -#if defined( __GNUC__ ) -#pragma GCC diagnostic pop -#elif defined( _MSC_VER ) -#pragma warning( pop ) -#endif - -#include -#endif - - -#include "MRMcp.h" - -#include "MRMesh/MRSystem.h" -#include "MRPch/MRSpdlog.h" -#include "MRViewer/MRCommandLoop.h" -#include "MRViewer/MRUITestEngineControl.h" -#include "MRViewer/MRViewer.h" - - - -/* HOW TO TEST MCP: - -Use "MCP Inspector". Run it with `npx @modelcontextprotocol/inspector`, where `npx` is installed as a part of Node.JS. -Set: - Transport Type = SSE - URL = http://localhost:8080/sse - Connection Type = Via Proxy (Doesn't work for me without proxy now when we're using the Fastmcpp library, but did work with another library; not sure why.) - -Press `Connect`. -Press `List Tools` (if grayed out, do `Clear` first). -Click on your tool. -On the right panel, set parameters. - For some parameter types, it helps to press `Switch to JSON` on the right, then type them as JSON. -Press `Run Tool`. - Note, you might need to press this twice. In some cases, the first press passes stale/empty parameters. -Then check for validation errors, below this button. - -If your output doesn't match the schema you specified, paste both the output and the schema (using the `Copy` button in the top-right corner of the code blocks; that copies JSON properly, unlike Ctrl+C in this case) - into a schema validator, e.g. https://www.jsonschemavalidator.net/ - -*/ - - -namespace MR -{ - -struct McpServer::State -{ - #if MR_ENABLE_MCP_SERVER - fastmcpp::tools::ToolManager tool_manager; // This has to be persistent, or `fastmcpp::mcp::make_mcp_handler()` dangles it. - fastmcpp::server::SseServerWrapper server; - - - State( int port, std::string name, std::string version, fastmcpp::tools::ToolManager new_tool_manager, const std::unordered_map& tool_descs ) - : tool_manager( std::move( new_tool_manager ) ), - server( fastmcpp::mcp::make_mcp_handler( name, version, tool_manager, tool_descs ), "127.0.0.1", port ) - {} - #endif -}; - -McpServer::McpServer() - : port_( 7887 ) // An arbirary value. -{ - recreateServer(); -} - -McpServer::McpServer( McpServer&& ) = default; -McpServer& McpServer::operator=( McpServer&& ) = default; -McpServer::~McpServer() = default; - -void McpServer::recreateServer() -{ - #if MR_ENABLE_MCP_SERVER - - fastmcpp::tools::ToolManager tool_manager; - std::unordered_map tool_descs; - - auto addTool = [&]( std::string id, std::string name, std::string desc, fastmcpp::Json input_schema, fastmcpp::Json output_schema, fastmcpp::tools::Tool::Fn func ) - { - tool_descs.try_emplace( id, desc ); // Why is this not automated by the library? - tool_manager.register_tool( fastmcpp::tools::Tool( id, input_schema, output_schema, func, name, desc, {} ) ); - }; - - static const auto skipFramesAfterInput = [] - { - for ( int i = 0; i < MR::getViewerInstance().forceRedrawMinimumIncrementAfterEvents; ++i ) - MR::CommandLoop::runCommandFromGUIThread( [] {} ); // Wait a few frames. - }; - - addTool( - /*id*/"ui.listEntries", - /*name*/"List UI entries", - /*desc*/"Returns the list of UI elements at the given path. The elements form a tree. Pass an empty array to get the top-level elements. Each element is described by a string. The path parameter describes the path from the root node to a specific element. Only elements of type `group` can have sub-elements.", - /*input_schema*/fastmcpp::Json::object( { - { "type", "object" }, - { "properties", fastmcpp::Json::object( { - { "path", fastmcpp::Json::object( { - { "type", "array" }, - { "items", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - } ) } - } ) }, - { "required", fastmcpp::Json::array( { - "path", - } ) }, - } ), - /*output_schema*/fastmcpp::Json::object( { - { "type", "array" }, - { "items", fastmcpp::Json::object( { - { "type", "object" }, - { "properties", fastmcpp::Json::object( { - { "name", fastmcpp::Json::object( { - { "type", "string" }, - } ) }, - { "type", fastmcpp::Json::object( { - { "type", "string" }, - } ) }, - } ) }, - { "required", fastmcpp::Json::array( { - "name", - "type", - } ) }, - } ) }, - } ), - /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json - { - if ( !params.contains( "path" ) ) - throw std::runtime_error( "The path parameter is missing." ); - - std::vector list; - MR::CommandLoop::runCommandFromGUIThread( [&] - { - auto ex = UI::TestEngine::Control::listEntries( params["path"].get>() ); - if ( !ex ) - throw std::runtime_error( ex.error() ); - list = std::move( *ex ); - } ); - - fastmcpp::Json ret = fastmcpp::Json::array(); - for ( const auto& elem : list ) - { - std::string typeStr; - switch ( elem.type ) - { - case UI::TestEngine::Control::EntryType::button: typeStr = "button"; break; - case UI::TestEngine::Control::EntryType::group: typeStr = "group"; break; - case UI::TestEngine::Control::EntryType::valueInt: typeStr = "int"; break; - case UI::TestEngine::Control::EntryType::valueUint: typeStr = "uint"; break; - case UI::TestEngine::Control::EntryType::valueReal: typeStr = "float"; break; // Hopefully "float" is more clear to LLMs than "real". The actual underlying type is `double`. - case UI::TestEngine::Control::EntryType::valueString: typeStr = "string"; break; - } - - assert( !typeStr.empty() ); - if ( typeStr.empty() ) - typeStr = "invalid"; - - ret.push_back( fastmcpp::Json::object( { - { "name", elem.name }, - { "type", std::move( typeStr ) }, - } ) ); - } - - return fastmcpp::Json::object( { { "result", ret } } ); - } - ); - - addTool( - /*id*/"ui.pressButton", - /*name*/"Press button", - /*desc*/"Presses the button at the given path.", - /*input_schema*/fastmcpp::Json::object( { - { "type", "object" }, - { "properties", fastmcpp::Json::object( { - { "path", fastmcpp::Json::object( { - { "type", "array" }, - { "items", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - } ) }, - } ) }, - { "required", fastmcpp::Json::array( { - "path", - } ) }, - } ), - /*output_schema*/fastmcpp::Json::object(), - /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json - { - if ( !params.contains( "path" ) ) - throw std::runtime_error( "The path parameter is missing." ); - - MR::CommandLoop::runCommandFromGUIThread( [&] - { - auto ex = UI::TestEngine::Control::pressButton( params["path"].get>() ); - if ( !ex ) - throw std::runtime_error( ex.error() ); - } ); - skipFramesAfterInput(); - - return fastmcpp::Json::object(); - } - ); - - auto handleValueType = [&]( const std::string& typeName ) - { - addTool( - /*id*/"ui.readValue" + typeName, - /*name*/"Read " + typeName + " value", - /*desc*/"Reads the value at the given path, of type `" + typeName + "`." + - ( - std::is_same_v - ? - " If the result contains an array called `allowedValues`, then when assigning a new value using `ui.writeValue" + typeName + "`, it must match one of the strings listed in `allowedValues`." - : - " When assigning a new value using `ui.writeValue" + typeName + "`, it must be between `min` and `max` inclusive." - ), - /*input_schema*/fastmcpp::Json::object( { - { "type", "object" }, - { "properties", fastmcpp::Json::object( { - { "path", fastmcpp::Json::object( { - { "type", "array" }, - { "items", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - } ) }, - } ) }, - { "required", fastmcpp::Json::array( { - "path", - } ) }, - } ), - /*output_schema*/( - std::is_same_v - ? - fastmcpp::Json::object( { - { "type", "object" }, - { "properties", fastmcpp::Json::object( { - { "value", fastmcpp::Json::object( { - { "type", "string" }, - } ) }, - { "allowedValues", fastmcpp::Json::object( { - { "type", "array" }, - { "items", fastmcpp::Json::object( { - { "type", "string" }, - } ) }, - } ) }, - } ) }, - { "required", fastmcpp::Json::array( { - "value", - } ) }, - } ) - : - fastmcpp::Json::object( { - { "type", "object" }, - { "properties", fastmcpp::Json::object( { - { "value", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - { "min", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - { "max", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - } ) }, - { "required", fastmcpp::Json::array( { - "value", - "min", - "max", - } ) }, - } ) - ), - /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json - { - if ( !params.contains( "path" ) ) - throw std::runtime_error( "The path parameter is missing." ); - - UI::TestEngine::Control::Value value; - MR::CommandLoop::runCommandFromGUIThread( [&] - { - auto ex = UI::TestEngine::Control::readValue( params["path"].get>() ); - if ( !ex ) - throw std::runtime_error( ex.error() ); - value = std::move( *ex ); - } ); - - fastmcpp::Json ret = fastmcpp::Json::object(); - ret["value"] = value.value; - if constexpr ( std::is_same_v ) - { - if ( value.allowedValues ) - ret["allowedValues"] = *value.allowedValues; - } - else - { - ret["min"] = value.min; - ret["max"] = value.max; - } - - return ret; - } - ); - - addTool( - /*id*/"ui.writeValue" + typeName, - /*name*/"Write " + typeName + " value", - /*desc*/"Writes the value at the given path, of type `" + typeName + "`. You can call `ui.readValue" + typeName + "` before this to know what values are allowed.", - /*input_schema*/fastmcpp::Json::object( { - { "type", "object" }, - { "properties", fastmcpp::Json::object( { - { "path", fastmcpp::Json::object( { - { "type", "array" }, - { "items", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - } ) }, - { "value", fastmcpp::Json::object( { - { "type", "number" }, - } ) }, - } ) }, - { "required", fastmcpp::Json::array( { - "path", - "value", - } ) }, - } ), - /*output_schema*/fastmcpp::Json::object(), - /*func*/[]( const fastmcpp::Json& params ) -> fastmcpp::Json - { - if ( !params.contains( "path" ) ) - throw std::runtime_error( "The path parameter is missing." ); - - if ( !params.contains( "path" ) ) - throw std::runtime_error( "The path parameter is missing." ); - if ( !params.contains( "value" ) ) - throw std::runtime_error( "The value parameter is missing." ); - - MR::CommandLoop::runCommandFromGUIThread( [&] - { - auto ex = UI::TestEngine::Control::writeValue( params["path"].get>(), T( params["value"] ) ); - if ( !ex ) - throw std::runtime_error( ex.error() ); - } ); - skipFramesAfterInput(); - - return fastmcpp::Json::object(); - } - ); - }; - - handleValueType.operator()( "Int" ); - handleValueType.operator()( "Uint" ); - handleValueType.operator()( "Real" ); - handleValueType.operator()( "String" ); - - state_ = std::make_unique( port_, "MeshInspector", GetMRVersionString(), std::move( tool_manager ), tool_descs ); - - #endif -} - -bool McpServer::isRunning() const -{ - #if MR_ENABLE_MCP_SERVER - return state_->server.running(); - #else - return false; - #endif -} - -bool McpServer::setRunning( bool enable ) -{ - #if MR_ENABLE_MCP_SERVER - if ( enable ) - { - bool ok = state_->server.start(); - if ( ok ) - spdlog::info( "MCP server started on port {}", getPort() ); - else - spdlog::error( "MCP server failed to start on port {}", getPort() ); - return ok; - } - else - { - state_->server.stop(); - spdlog::info( "MCP server stopped" ); - return true; - } - #else - (void)enable; - return false; - #endif -} - -McpServer& getDefaultMcpServer() -{ - static McpServer ret = [] - { - McpServer server; - server.setRunning( true ); - return server; - }(); - return ret; -} - -#if MR_ENABLE_MCP_SERVER -static const std::nullptr_t init_mcp = []{ - // Poke the default MCP server to start it. - // Use `CommandLoop` to delay initialization until the viewer finishes initializing. - // Otherwise this gets called too early, before even the logger is configured. - CommandLoop::appendCommand( []{ (void)getDefaultMcpServer(); } ); - - return nullptr; -}(); -#endif - -} // namespace MR diff --git a/source/MRViewer/MRMcp.h b/source/MRViewer/MRMcp.h deleted file mode 100644 index 1c484cf4e14f..000000000000 --- a/source/MRViewer/MRMcp.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "exports.h" - -#include - -namespace MR -{ - -class McpServer -{ - struct State; - - int port_ = -1; // See the constructor. - std::unique_ptr state_; - -public: - MRVIEWER_API McpServer(); - MRVIEWER_API McpServer( McpServer&& ); - MRVIEWER_API McpServer& operator=( McpServer&& ); - MRVIEWER_API ~McpServer(); - - [[nodiscard]] int getPort() const { return port_; } - void setPort( int port ) { port_ = port; } - - // This stops the server and applies the new settings, such as the port. - // Then call `setRunning( true )` to start it again. - MRVIEWER_API void recreateServer(); - - [[nodiscard]] MRVIEWER_API bool isRunning() const; - // Returns true on success, including if the server is already running and you're trying to start it again. - // Stopping always returns true. - MRVIEWER_API bool setRunning( bool enable ); -}; - -[[nodiscard]] MRVIEWER_API McpServer& getDefaultMcpServer(); - -} // namespace MR diff --git a/source/MRViewer/MRSetupMcp.cpp b/source/MRViewer/MRSetupMcp.cpp new file mode 100644 index 000000000000..76c3433110f9 --- /dev/null +++ b/source/MRViewer/MRSetupMcp.cpp @@ -0,0 +1,15 @@ +#include "MRMcp/MRMcp.h" +#include "MRMesh/MROnInit.h" +#include "MRViewer/MRCommandLoop.h" + +namespace MR +{ + +MR_ON_INIT{ + MR::CommandLoop::appendCommand( [] + { + Mcp::getDefaultServer().setRunning( true ); + } ); +}; + +} // namespace MR diff --git a/source/MRViewer/MRViewer.vcxproj b/source/MRViewer/MRViewer.vcxproj index 0baf193f0ed2..8f13971773b3 100644 --- a/source/MRViewer/MRViewer.vcxproj +++ b/source/MRViewer/MRViewer.vcxproj @@ -160,7 +160,7 @@ - + @@ -342,7 +342,6 @@ - @@ -477,8 +476,8 @@ {7cc4f0fe-ace6-4441-9dd7-296066b6d69f} - - {7853aec9-a364-4587-89ae-faa9a463e6ed} + + {c8250f26-e01d-4a63-98cd-68069d818080} diff --git a/source/MRViewer/MRViewerMcp.cpp b/source/MRViewer/MRViewerMcp.cpp new file mode 100644 index 000000000000..d20e39264dbf --- /dev/null +++ b/source/MRViewer/MRViewerMcp.cpp @@ -0,0 +1,178 @@ +#ifndef MESHLIB_NO_MCP + +#include "MRMcp/MRMcp.h" +#include "MRMesh/MROnInit.h" +#include "MRViewer/MRCommandLoop.h" +#include "MRViewer/MRUITestEngineControl.h" +#include "MRViewer/MRViewer.h" + +namespace MR +{ + +static void skipFramesAfterInput() +{ + for ( int i = 0; i < MR::getViewerInstance().forceRedrawMinimumIncrementAfterEvents; ++i ) + MR::CommandLoop::runCommandFromGUIThread( [] {} ); // Wait a few frames. +} + +static nlohmann::json mcpToolListUiEntries( const nlohmann::json& args ) +{ + std::vector list; + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::listEntries( args.at( "path" ).get>() ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + list = std::move( *ex ); + } ); + + nlohmann::json ret = nlohmann::json::array(); + for ( const auto& elem : list ) + { + std::string typeStr; + switch ( elem.type ) + { + case UI::TestEngine::Control::EntryType::button: typeStr = "button"; break; + case UI::TestEngine::Control::EntryType::group: typeStr = "group"; break; + case UI::TestEngine::Control::EntryType::valueInt: typeStr = "int"; break; + case UI::TestEngine::Control::EntryType::valueUint: typeStr = "uint"; break; + case UI::TestEngine::Control::EntryType::valueReal: typeStr = "float"; break; // Hopefully "float" is more clear to LLMs than "real". The actual underlying type is `double`. + case UI::TestEngine::Control::EntryType::valueString: typeStr = "string"; break; + } + + assert( !typeStr.empty() ); + if ( typeStr.empty() ) + typeStr = "invalid"; + + ret.push_back( nlohmann::json::object( { + { "name", elem.name }, + { "type", std::move( typeStr ) }, + } ) ); + } + + return nlohmann::json::object( { { "result", ret } } ); +} + +static nlohmann::json mcpToolPressButton( const nlohmann::json& args ) +{ + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::pressButton( args.at( "path" ).get>() ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + } ); + skipFramesAfterInput(); + + return nlohmann::json::object(); +} + +template +static nlohmann::json mcpToolReadValue( const nlohmann::json& args ) +{ + UI::TestEngine::Control::Value value; + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::readValue( args.at( "path" ).get>() ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + value = std::move( *ex ); + } ); + + nlohmann::json ret = nlohmann::json::object(); + ret["value"] = value.value; + if constexpr ( std::is_same_v ) + { + if ( value.allowedValues ) + ret["allowedValues"] = *value.allowedValues; + } + else + { + ret["min"] = value.min; + ret["max"] = value.max; + } + + return ret; +} + +template +static nlohmann::json mcpToolWriteValue( const nlohmann::json& args ) +{ + MR::CommandLoop::runCommandFromGUIThread( [&] + { + auto ex = UI::TestEngine::Control::writeValue( args.at( "path" ).get>(), T( args.at( "value" ) ) ); + if ( !ex ) + throw std::runtime_error( ex.error() ); + } ); + skipFramesAfterInput(); + + return nlohmann::json::object(); +} + +MR_ON_INIT{ + Mcp::Server& server = Mcp::getDefaultServer(); + + server.addTool( + /*id*/"ui.listEntries", + /*name*/"List UI entries", + /*desc*/"Returns the list of UI elements at the given path. The elements form a tree. Pass an empty array to get the top-level elements. Each element is described by a string. The path parameter describes the path from the root node to a specific element. Only elements of type `group` can have sub-elements.", + /*input_schema*/Mcp::Schema::Object{}.addMember( "path", Mcp::Schema::Array( Mcp::Schema::String{} ) ), + /*output_schema*/Mcp::Schema::Array( Mcp::Schema::Object{}.addMember( "name", Mcp::Schema::String{} ).addMember( "type", Mcp::Schema::String{} ) ), + /*func*/mcpToolListUiEntries + ); + + server.addTool( + /*id*/"ui.pressButton", + /*name*/"Press button", + /*desc*/"Presses the button at the given path.", + /*input_schema*/Mcp::Schema::Object{}.addMember( "path", Mcp::Schema::Array( Mcp::Schema::String{} ) ), + /*output_schema*/Mcp::Schema::Empty{}, + /*func*/mcpToolPressButton + ); + + auto handleValueType = [&]( const std::string& typeName ) + { + server.addTool( + /*id*/"ui.readValue" + typeName, + /*name*/"Read " + typeName + " value", + /*desc*/"Reads the value at the given path, of type `" + typeName + "`." + + ( + std::is_same_v + ? + " If the result contains an array called `allowedValues`, then when assigning a new value using `ui.writeValue" + typeName + "`, it must match one of the strings listed in `allowedValues`." + : + " When assigning a new value using `ui.writeValue" + typeName + "`, it must be between `min` and `max` inclusive." + ), + /*input_schema*/Mcp::Schema::Object{}.addMember( "path", Mcp::Schema::Array( Mcp::Schema::String{} ) ), + /*output_schema*/( + std::is_same_v + ? + static_cast( + Mcp::Schema::Object{}.addMember( "value", Mcp::Schema::String{} ).addMemberOpt( "allowedValues", Mcp::Schema::Array( Mcp::Schema::String{} ) ) + ) + : + static_cast( + Mcp::Schema::Object{}.addMember( "value", Mcp::Schema::Number{} ).addMember( "min", Mcp::Schema::Number{} ).addMember( "max", Mcp::Schema::Number{} ) + ) + ), + /*func*/mcpToolReadValue + ); + + server.addTool( + /*id*/"ui.writeValue" + typeName, + /*name*/"Write " + typeName + " value", + /*desc*/"Writes the value at the given path, of type `" + typeName + "`. You can call `ui.readValue" + typeName + "` before this to know what values are allowed.", + /*input_schema*/Mcp::Schema::Object{}.addMember( "path", Mcp::Schema::Array( Mcp::Schema::String{} ) ).addMember( "value", std::is_same_v ? static_cast( Mcp::Schema::String{} ) : static_cast( Mcp::Schema::Number{} ) ), + /*output_schema*/Mcp::Schema::Empty{}, + /*func*/mcpToolWriteValue + ); + }; + + handleValueType.operator()( "Int" ); + handleValueType.operator()( "Uint" ); + handleValueType.operator()( "Real" ); + handleValueType.operator()( "String" ); +}; // MR_ON_INIT + +} // namespace MR + +#endif diff --git a/source/MeshLib.sln b/source/MeshLib.sln index 74411f8f087c..967ecf9668d2 100644 --- a/source/MeshLib.sln +++ b/source/MeshLib.sln @@ -64,6 +64,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MRTestCuda", "MRTestCuda\MR EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fastmcpp", "fastmcpp\fastmcpp.vcxproj", "{7853AEC9-A364-4587-89AE-FAA9A463E6ED}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MRMcp", "MRMcp\MRMcp.vcxproj", "{C8250F26-E01D-4A63-98CD-68069D818080}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -158,6 +160,10 @@ Global {7853AEC9-A364-4587-89AE-FAA9A463E6ED}.Debug|x64.Build.0 = Debug|x64 {7853AEC9-A364-4587-89AE-FAA9A463E6ED}.Release|x64.ActiveCfg = Release|x64 {7853AEC9-A364-4587-89AE-FAA9A463E6ED}.Release|x64.Build.0 = Release|x64 + {C8250F26-E01D-4A63-98CD-68069D818080}.Debug|x64.ActiveCfg = Debug|x64 + {C8250F26-E01D-4A63-98CD-68069D818080}.Debug|x64.Build.0 = Debug|x64 + {C8250F26-E01D-4A63-98CD-68069D818080}.Release|x64.ActiveCfg = Release|x64 + {C8250F26-E01D-4A63-98CD-68069D818080}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -185,6 +191,7 @@ Global {E0202297-EDB2-4CDC-9CD0-8921EFF08DA0} = {AE8B4895-7920-4AD3-B554-C858A08B1680} {FFB8D063-FF1E-4F18-8479-249B36714EF7} = {E0BE85ED-C366-40EF-8BDE-70E1EDC8860F} {7853AEC9-A364-4587-89AE-FAA9A463E6ED} = {AE8B4895-7920-4AD3-B554-C858A08B1680} + {C8250F26-E01D-4A63-98CD-68069D818080} = {AE8B4895-7920-4AD3-B554-C858A08B1680} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6F7912D7-5687-4CBB-828B-1BEDD18B8249} diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index 3b6b5f8a5d1c..776c3718fa65 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit 3b6b5f8a5d1c332cd61babf1bc49048fe2af9e5a +Subproject commit 776c3718fa650f7a58206110ef2d444beed72a80 From c4e3f0646e8ce7ee98bd96c6090682b054c669b0 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Wed, 15 Apr 2026 10:47:10 -0500 Subject: [PATCH 24/45] Move the MCP testing guide to a separate file. --- docs/testing_mcp.md | 25 +++++++++++++++++++++++++ source/MRMcp/MRMcp.cpp | 23 ----------------------- source/MRMcp/MRMcp.h | 1 + 3 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 docs/testing_mcp.md diff --git a/docs/testing_mcp.md b/docs/testing_mcp.md new file mode 100644 index 000000000000..fc37f01969d8 --- /dev/null +++ b/docs/testing_mcp.md @@ -0,0 +1,25 @@ +# How to test MCP? + +Use "MCP Inspector". Run it with `npx @modelcontextprotocol/inspector`, where `npx` is installed as a part of Node.JS. + +Set: +* Transport Type = SSE +* URL = http://localhost:8080/sse +* Connection Type = Via Proxy (Doesn't work for me without proxy now when we're using the Fastmcpp library, but did work with another library; not sure why.) + +Press `Connect`. + +Press `List Tools` (if grayed out, do `Clear` first). + +Click on your tool. + +On the right panel, set parameters. For some parameter types, it helps to press `Switch to JSON` on the right, then type them as JSON. + +Press `Run Tool`. If you get weird errors, try pressing it again. In some cases, the first press passes stale/empty parameters. + +Then check for validation errors, below this button. + +If it complains that your output doesn't match the schema you specified, paste both the output and the schema (using the `Copy` button in the top-right corner of the code blocks; that copies JSON properly, unlike Ctrl+C in this case) + into a schema validator, e.g. https://www.jsonschemavalidator.net/ + +**NOTE:** It doesn't seem to validate the input schema (only output schema). Check it by eye. diff --git a/source/MRMcp/MRMcp.cpp b/source/MRMcp/MRMcp.cpp index f4d7de9db175..25abda8ddb08 100644 --- a/source/MRMcp/MRMcp.cpp +++ b/source/MRMcp/MRMcp.cpp @@ -33,29 +33,6 @@ #include -/* HOW TO TEST MCP: - -Use "MCP Inspector". Run it with `npx @modelcontextprotocol/inspector`, where `npx` is installed as a part of Node.JS. -Set: - Transport Type = SSE - URL = http://localhost:8080/sse - Connection Type = Via Proxy (Doesn't work for me without proxy now when we're using the Fastmcpp library, but did work with another library; not sure why.) - -Press `Connect`. -Press `List Tools` (if grayed out, do `Clear` first). -Click on your tool. -On the right panel, set parameters. - For some parameter types, it helps to press `Switch to JSON` on the right, then type them as JSON. -Press `Run Tool`. - Note, you might need to press this twice. In some cases, the first press passes stale/empty parameters. -Then check for validation errors, below this button. - -If your output doesn't match the schema you specified, paste both the output and the schema (using the `Copy` button in the top-right corner of the code blocks; that copies JSON properly, unlike Ctrl+C in this case) - into a schema validator, e.g. https://www.jsonschemavalidator.net/ - -*/ - - namespace MR::Mcp { diff --git a/source/MRMcp/MRMcp.h b/source/MRMcp/MRMcp.h index 9673833a0fae..34ad5f4eac87 100644 --- a/source/MRMcp/MRMcp.h +++ b/source/MRMcp/MRMcp.h @@ -128,6 +128,7 @@ class Server // Fails if the tool with this `id` already exists. // Must be called early, before `setRunning(true)` is called for the first time, otherwise fails. // Returns true on success. Asserts when returning false, so you don't have to check the return value. + // NOTE: Consult `docs/testing_mcp.md` for how to test your tool. MRMCP_API bool addTool( std::string id, std::string name, std::string desc, Schema::Base inputSchema, Schema::Base outputSchema, ToolFunc func ); [[nodiscard]] MRMCP_API Params getParams() const; From c27f5fe5e4797f4556c03d29b6c0d91347279540 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Wed, 15 Apr 2026 10:55:33 -0500 Subject: [PATCH 25/45] Try again. --- source/MRViewer/MRViewer.vcxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/source/MRViewer/MRViewer.vcxproj b/source/MRViewer/MRViewer.vcxproj index 8f13971773b3..d90155168e18 100644 --- a/source/MRViewer/MRViewer.vcxproj +++ b/source/MRViewer/MRViewer.vcxproj @@ -160,6 +160,7 @@ + From 0c12532b60280a15bec889b39ef4cb330ac3b418 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Wed, 15 Apr 2026 11:28:07 -0500 Subject: [PATCH 26/45] Address some comments. --- source/MRMcp/MRMcp.h | 50 ++++++++++++++++--------- source/MRViewer/MRUITestEngineControl.h | 4 +- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/source/MRMcp/MRMcp.h b/source/MRMcp/MRMcp.h index 34ad5f4eac87..e7a38ac638f7 100644 --- a/source/MRMcp/MRMcp.h +++ b/source/MRMcp/MRMcp.h @@ -10,9 +10,10 @@ namespace MR::Mcp { -// This is used to build json schemas. +/// This is used to build json schemas. namespace Schema { + /// A common base class for the different schemas. Functions can accept this by value, it's fine to slice it. struct Base { protected: @@ -24,11 +25,13 @@ namespace Schema [[nodiscard]] nlohmann::json&& asJson() && { return std::move( json ); } }; + /// An empty schema. struct Empty : Base { Empty() : Base( {} ) {} }; + /// A schema describing a scalar. struct Number : Base { Number() @@ -38,6 +41,7 @@ namespace Schema {} }; + /// A schema describing a string. struct String : Base { String() @@ -47,6 +51,7 @@ namespace Schema {} }; + /// A schema describing an array of whatever is passed to the constructor. struct Array : Base { Array( Base elemSchema ) @@ -57,6 +62,8 @@ namespace Schema {} }; + /// A schema describing an object. + /// Construct like this: `Object{}.addMember(...).addMember(...)`. struct Object : Base { Object() @@ -67,27 +74,27 @@ namespace Schema } ) ) {} - // Add required member. Returns a reference to `*this`. + /// Add required member. Returns a reference to `*this`. Object &addMember( std::string name, Base schema ) & { json.at("required").push_back( name ); addMemberOpt( std::move( name ), std::move( schema ) ); return *this; } - // Add optional member. Returns a reference to `*this`. + /// Add optional member. Returns a reference to `*this`. Object &addMemberOpt( std::string name, Base schema ) & { json.at( "properties" ).push_back( nlohmann::json::object_t::value_type( std::move( name ), std::move( schema ).asJson() ) ); return *this; } - // Add required member. Returns a reference to `*this`. + /// Add required member. Returns a reference to `*this`. [[nodiscard]] Object&& addMember( std::string name, Base schema ) && { addMember( std::move( name ), std::move( schema ) ); return std::move( *this ); } - // Add optional member. Returns a reference to `*this`. + /// Add optional member. Returns a reference to `*this`. [[nodiscard]] Object&& addMemberOpt( std::string name, Base schema ) && { addMemberOpt( std::move( name ), std::move( schema ) ); @@ -96,20 +103,21 @@ namespace Schema }; } // namespace Schema +/// Owns a HTTP MCP server (using the SSE protocol). class Server { struct State; - // This is null until either `setParams()` or `setRunning(true)` is called for the first time. + /// This is null until either `setParams()` or `setRunning(true)` is called for the first time. std::unique_ptr state_; public: struct Params { - std::string address = "127.0.0.1"; // You don't need to change this, unless you want to accept connections from the outside world. + std::string address = "127.0.0.1"; ///< You don't need to change this, unless you want to accept connections from the outside world. int port = 7887; - std::string name; // A default string is set in the constructor. - std::string version; // A default string is set in the constructor. + std::string name; ///< A default string is set in the constructor. + std::string version; ///< A default string is set in the constructor. friend bool operator==( const Params&, const Params& ) = default; @@ -121,27 +129,33 @@ class Server MRMCP_API Server& operator=( Server&& ); MRMCP_API ~Server(); - // Those functions are allowed to throw, that's how you report errors to the MCP. + /// Those functions are allowed to throw, that's how you report errors to the MCP. using ToolFunc = std::function; - // Registers a new tool. - // Fails if the tool with this `id` already exists. - // Must be called early, before `setRunning(true)` is called for the first time, otherwise fails. - // Returns true on success. Asserts when returning false, so you don't have to check the return value. - // NOTE: Consult `docs/testing_mcp.md` for how to test your tool. + /// Registers a new tool. + /// @param id An arbitrary function name, e.g. `foo.bar`. + /// @param name A human/ai-readable name. + /// @param desc A human/ai-readable explanation of what the tool does. + /// @param inputSchema Describes the arguments. Normally it should be `Schema::Object{}` with some fields added. + /// @param outputSchema Describes the returned JSON. + /// Fails if the tool with this `id` already exists. + /// Must be called early, before `setRunning(true)` is called for the first time, otherwise fails. + /// Returns true on success. Asserts when returning false, so you don't have to check the return value. + /// NOTE: Consult `docs/testing_mcp.md` for how to test your tool. MRMCP_API bool addTool( std::string id, std::string name, std::string desc, Schema::Base inputSchema, Schema::Base outputSchema, ToolFunc func ); [[nodiscard]] MRMCP_API Params getParams() const; - // This restarts the server if necessary. + /// This restarts the server if necessary. MRMCP_API void setParams( Params params ); [[nodiscard]] MRMCP_API bool isRunning() const; - // Returns true on success, including if the server is already running and you're trying to start it again. - // Stopping always returns true. + /// Returns true on success, including if the server is already running and you're trying to start it again. + /// Stopping always returns true. MRMCP_API bool setRunning( bool enable ); }; +/// The global instance of the MCP server. [[nodiscard]] MRMCP_API Server& getDefaultServer(); } // namespace MR diff --git a/source/MRViewer/MRUITestEngineControl.h b/source/MRViewer/MRUITestEngineControl.h index 37a131436224..20a0286b7fcc 100644 --- a/source/MRViewer/MRUITestEngineControl.h +++ b/source/MRViewer/MRUITestEngineControl.h @@ -12,8 +12,8 @@ namespace MR::UI::TestEngine::Control { // Most changes in this file must be synced with: -// * Python: `MeshLib/source/mrviewerpy/MRPythonUiInteraction.cpp`. -// * MCP: `source/MRInspector/MRMcp.cpp`. +// * Python: `source/mrviewerpy/MRPythonUiInteraction.cpp`. +// * MCP: `source/MRViewer/MRViewerMcp.cpp`. enum class EntryType { From 037b885e767e64b6c8b3d240db452f6fb5bff4e5 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 05:38:27 -0500 Subject: [PATCH 27/45] Try again. --- source/MRViewer/MRSetupMcp.cpp | 15 --------------- source/MRViewer/MRSetupViewer.cpp | 14 ++++++++++++++ source/MRViewer/MRSetupViewer.h | 4 ++++ source/MRViewer/MRViewer.cpp | 13 +++++++------ source/MRViewer/MRViewer.vcxproj | 1 - thirdparty/fastmcpp | 2 +- 6 files changed, 26 insertions(+), 23 deletions(-) delete mode 100644 source/MRViewer/MRSetupMcp.cpp diff --git a/source/MRViewer/MRSetupMcp.cpp b/source/MRViewer/MRSetupMcp.cpp deleted file mode 100644 index 76c3433110f9..000000000000 --- a/source/MRViewer/MRSetupMcp.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "MRMcp/MRMcp.h" -#include "MRMesh/MROnInit.h" -#include "MRViewer/MRCommandLoop.h" - -namespace MR -{ - -MR_ON_INIT{ - MR::CommandLoop::appendCommand( [] - { - Mcp::getDefaultServer().setRunning( true ); - } ); -}; - -} // namespace MR diff --git a/source/MRViewer/MRSetupViewer.cpp b/source/MRViewer/MRSetupViewer.cpp index aaf95506eb25..94dec311540d 100644 --- a/source/MRViewer/MRSetupViewer.cpp +++ b/source/MRViewer/MRSetupViewer.cpp @@ -19,6 +19,10 @@ #include #endif +#ifndef MESHLIB_NO_MCP +#include "MRMcp/MRMcp.h" +#endif + namespace MR { @@ -189,4 +193,14 @@ void ViewerSetup::unloadExtendedLibraries() const #endif // ifndef __EMSCRIPTEN__ } +bool ViewerSetup::setupMcp() const +{ + #ifndef MESHLIB_NO_MCP + Mcp::getDefaultServer().setRunning( true ); + return true; + #else + return false; + #endif +} + } //namespace MR diff --git a/source/MRViewer/MRSetupViewer.h b/source/MRViewer/MRSetupViewer.h index 3b63c3a4e506..add0fdd6a90a 100644 --- a/source/MRViewer/MRSetupViewer.h +++ b/source/MRViewer/MRSetupViewer.h @@ -43,6 +43,10 @@ class MRVIEWER_CLASS ViewerSetup /// free all libraries loaded in setupExtendedLibraries() MRVIEWER_API virtual void unloadExtendedLibraries() const; + /// Launch the MCP server. Append this to the `CommandLoop` instead of calling immediately. + /// Returns false if the MCP support is not compiled in. + MRVIEWER_API virtual bool setupMcp() const; + // functor to setup custom log sink, i.e. sending logs to web std::function setupCustomLogSink; diff --git a/source/MRViewer/MRViewer.cpp b/source/MRViewer/MRViewer.cpp index a4b9e8cd0bba..905aa8e774a4 100644 --- a/source/MRViewer/MRViewer.cpp +++ b/source/MRViewer/MRViewer.cpp @@ -226,14 +226,14 @@ static void glfw_window_pos( GLFWwindow* /*window*/, int xPos, int yPos ) } ); // It is necessary to redraw the contents of the window when moving the window in Windows OS - // + // // (on Windows) The glfw_window_pos callback is called, but glfwWaitEvents does not pass, // and event queue processing is not performed until the end of the move. // For this reason, draw is called outside of EventQueue. - // + // // "On some platforms, a window move, resize or menu operation will cause event processing to block. This is due to how event processing is designed on those platforms" // https://www.glfw.org/docs/latest/group__window.html#ga37bd57223967b4211d60ca1a0bf3c832 - // + // // https://stackoverflow.com/questions/71243906/glfw-window-poll-events-lag #ifdef _WIN32 viewer->draw( true ); @@ -393,6 +393,7 @@ int launchDefaultViewer( const Viewer::LaunchParams& params, const ViewerSetup& CommandLoop::appendCommand( [&] () { setup.setupExtendedLibraries(); + setup.setupMcp(); }, CommandLoop::StartPosition::AfterSplashAppear ); int res = 0; @@ -634,10 +635,10 @@ int Viewer::launch( const LaunchParams& params ) isAnimating = params.isAnimating; animationMaxFps = params.animationMaxFps; experimentalFeatures = params.developerFeatures; - + bool defaultMultiViewport = Config::instance().getBool( cDefaultMultiViewportKey, true ); launchParams_.multiViewport = defaultMultiViewport && params.multiViewport; - + auto res = launchInit_( params ); if ( res != EXIT_SUCCESS ) return res; @@ -914,7 +915,7 @@ int Viewer::launchInit_( const LaunchParams& params ) params.splashWindow->start(); continueTime = std::chrono::steady_clock::now() + std::chrono::duration( params.splashWindow->minimumTimeSec() ); } - + CommandLoop::setState( CommandLoop::StartPosition::AfterSplashAppear ); CommandLoop::processCommands(); diff --git a/source/MRViewer/MRViewer.vcxproj b/source/MRViewer/MRViewer.vcxproj index d90155168e18..7ff63089ef77 100644 --- a/source/MRViewer/MRViewer.vcxproj +++ b/source/MRViewer/MRViewer.vcxproj @@ -161,7 +161,6 @@ - diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index 776c3718fa65..3f207765ba2e 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit 776c3718fa650f7a58206110ef2d444beed72a80 +Subproject commit 3f207765ba2e9d055a928f8911ce5649e893813f From 68b8d1349e8cd7524276df52c111d706c4b77df0 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 06:37:24 -0500 Subject: [PATCH 28/45] More code changes. --- source/MRMcp/MRMcp.cpp | 20 +++++++++----------- source/MRMcp/MRMcp.h | 15 +++++++++------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/source/MRMcp/MRMcp.cpp b/source/MRMcp/MRMcp.cpp index 25abda8ddb08..fdc76f232446 100644 --- a/source/MRMcp/MRMcp.cpp +++ b/source/MRMcp/MRMcp.cpp @@ -38,12 +38,11 @@ namespace MR::Mcp struct Server::State { - Params params; fastmcpp::tools::ToolManager toolManager; // This has to be persistent, or `fastmcpp::mcp::make_mcp_handler()` dangles it. std::unordered_map toolDescs; // No idea why this is not a part of `toolManager`. std::optional server; - void createServer() + void createServer( const Params& params ) { assert( !server ); server.emplace( fastmcpp::mcp::make_mcp_handler( params.name, params.version, toolManager, toolDescs ), params.address, params.port ); @@ -83,26 +82,25 @@ bool Server::addTool( std::string id, std::string name, std::string desc, Schema return true; } -Server::Params Server::getParams() const +const Server::Params& Server::getParams() const { - if ( state_ ) - return state_->params; - else - return {}; + return params_; } void Server::setParams( Server::Params params ) { const bool serverExisted = state_ && bool( state_->server ); const bool serverWasRunning = serverExisted && isRunning(); + if ( serverWasRunning ) setRunning( false ); + if ( serverExisted ) + state_->server.reset(); - state_->server.reset(); - state_->params = std::move( params ); + params_ = std::move( params ); if ( serverExisted ) - state_->createServer(); + state_->createServer( params ); if ( serverWasRunning ) setRunning( true ); } @@ -119,7 +117,7 @@ bool Server::setRunning( bool enable ) if ( !state_ ) state_ = std::make_unique(); if ( !state_->server ) - state_->createServer(); + state_->createServer( params_ ); bool ok = state_->server->start(); if ( ok ) diff --git a/source/MRMcp/MRMcp.h b/source/MRMcp/MRMcp.h index e7a38ac638f7..acb80d04395d 100644 --- a/source/MRMcp/MRMcp.h +++ b/source/MRMcp/MRMcp.h @@ -106,11 +106,6 @@ namespace Schema /// Owns a HTTP MCP server (using the SSE protocol). class Server { - struct State; - - /// This is null until either `setParams()` or `setRunning(true)` is called for the first time. - std::unique_ptr state_; - public: struct Params { @@ -144,7 +139,7 @@ class Server /// NOTE: Consult `docs/testing_mcp.md` for how to test your tool. MRMCP_API bool addTool( std::string id, std::string name, std::string desc, Schema::Base inputSchema, Schema::Base outputSchema, ToolFunc func ); - [[nodiscard]] MRMCP_API Params getParams() const; + [[nodiscard]] MRMCP_API const Params& getParams() const; /// This restarts the server if necessary. MRMCP_API void setParams( Params params ); @@ -153,6 +148,14 @@ class Server /// Returns true on success, including if the server is already running and you're trying to start it again. /// Stopping always returns true. MRMCP_API bool setRunning( bool enable ); + +private: + struct State; + + /// This is null until either `setParams()` or `setRunning(true)` is called for the first time. + std::unique_ptr state_; + + Params params_; }; /// The global instance of the MCP server. From 4087c37f1410aed731ef7fa306e6cc59d35e47d1 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 06:54:40 -0500 Subject: [PATCH 29/45] Try again. --- CMakeLists.txt | 2 +- scripts/build_thirdparty.sh | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 48a6f9ff21b3..6ab4c2cb5ba8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,7 +296,7 @@ ENDIF() IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. - IF(MESHLIB_USE_VCPKG) + IF(MESHLIB_USE_VCPKG OR APPLE) add_subdirectory(../fastmcpp fastmcpp) ELSE() find_package(fastmcpp REQUIRED) diff --git a/scripts/build_thirdparty.sh b/scripts/build_thirdparty.sh index c245b64a260d..645a25fabcdc 100755 --- a/scripts/build_thirdparty.sh +++ b/scripts/build_thirdparty.sh @@ -138,12 +138,16 @@ else # build clip separately CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/clip.sh ${MESHLIB_THIRDPARTY_DIR}/clip - # Build nlohmann-json separately. It is header-only, this just installs it. It is a dependency of fastmcpp. - CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/nlohmann-json.sh "$MESHLIB_THIRDPARTY_DIR/nlohmann-json" - # Build cpp-httplib separately. It is header-only, this just installs it. It is a dependency of fastmcpp. - CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/cpp-httplib.sh "$MESHLIB_THIRDPARTY_DIR/cpp-httplib" - # Build fastmcpp separately. - CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/fastmcpp.sh "$MESHLIB_THIRDPARTY_DIR/fastmcpp" ./fastmcpp_build "${MESHLIB_THIRDPARTY_ROOT_DIR}" + # Skip this on Mac, we use `add_subdirectory()` for those libraries there. + # This is because we can't use `find_package()` there to find our own libraries, because that breaks Python modules, as documented in the root `CMakeLists.txt`. + if [[ $OSTYPE == 'darwin'* ]]; then + # Build nlohmann-json separately. It is header-only, this just installs it. It is a dependency of fastmcpp. + CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/nlohmann-json.sh "$MESHLIB_THIRDPARTY_DIR/nlohmann-json" + # Build cpp-httplib separately. It is header-only, this just installs it. It is a dependency of fastmcpp. + CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/cpp-httplib.sh "$MESHLIB_THIRDPARTY_DIR/cpp-httplib" + # Build fastmcpp separately. + CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/fastmcpp.sh "$MESHLIB_THIRDPARTY_DIR/fastmcpp" ./fastmcpp_build "${MESHLIB_THIRDPARTY_ROOT_DIR}" + fi fi popd From a79563312f26c91baed90392a4593bb5631d395a Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 07:16:13 -0500 Subject: [PATCH 30/45] Try again. --- .github/workflows/build-test-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index 1800afe27fb3..907955d3adbe 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -79,7 +79,7 @@ jobs: - name: Checkout third-party submodules run: | # Download sub-submodules for certain submodules. We don't recurse above in Checkout to improve build performance. See: https://github.com/actions/checkout/issues/1779 - git submodule update --init --recursive --depth 1 thirdparty/mrbind + git submodule update --init --recursive --depth 1 thirdparty/mrbind thirdparty/fastmcpp thirdparty/cpp-httplib thirdparty/nlohmann-json - name: Setup Homebrew prefix and resolve compiler paths id: setup From 367a4fcf99caf91df282e3745eb7ba67b4ead52f Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 07:35:54 -0500 Subject: [PATCH 31/45] Try again. --- .github/workflows/build-test-macos.yml | 2 +- .github/workflows/build-test-windows.yml | 2 +- CMakeLists.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index 907955d3adbe..1800afe27fb3 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -79,7 +79,7 @@ jobs: - name: Checkout third-party submodules run: | # Download sub-submodules for certain submodules. We don't recurse above in Checkout to improve build performance. See: https://github.com/actions/checkout/issues/1779 - git submodule update --init --recursive --depth 1 thirdparty/mrbind thirdparty/fastmcpp thirdparty/cpp-httplib thirdparty/nlohmann-json + git submodule update --init --recursive --depth 1 thirdparty/mrbind - name: Setup Homebrew prefix and resolve compiler paths id: setup diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml index e7ad08a456b3..f4b2356353bf 100644 --- a/.github/workflows/build-test-windows.yml +++ b/.github/workflows/build-test-windows.yml @@ -56,7 +56,7 @@ jobs: - name: Checkout third-party submodules run: | # Download sub-submodules for certain submodules. We don't recurse above in Checkout to improve build performance. See: https://github.com/actions/checkout/issues/1779 - git submodule update --init --recursive --depth 1 thirdparty/mrbind thirdparty/fastmcpp thirdparty/nlohmann-json thirdparty/cpp-httplib + git submodule update --init --recursive --depth 1 thirdparty/mrbind - name: Get AWS instance type uses: ./.github/actions/get-aws-instance-type diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ab4c2cb5ba8..90c662da52d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -297,7 +297,7 @@ ENDIF() IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. IF(MESHLIB_USE_VCPKG OR APPLE) - add_subdirectory(../fastmcpp fastmcpp) + add_subdirectory(${MESHLIB_THIRDPARTY_DIR}/fastmcpp fastmcpp) ELSE() find_package(fastmcpp REQUIRED) ENDIF() From 1acee2c3481d251c99553d69ac6dbc966cdb1df9 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 07:58:43 -0500 Subject: [PATCH 32/45] Try to fix Mac builds. --- CMakeLists.txt | 3 +++ thirdparty/fastmcpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 90c662da52d0..a7c4ebb4d68c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -297,6 +297,9 @@ ENDIF() IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. IF(MESHLIB_USE_VCPKG OR APPLE) + IF(APPLE) + set(FASTMCPP_DEPS_ADD_SUBDIRECTORY ON) + ENDIF() add_subdirectory(${MESHLIB_THIRDPARTY_DIR}/fastmcpp fastmcpp) ELSE() find_package(fastmcpp REQUIRED) diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index 3f207765ba2e..d2ad55a3460a 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit 3f207765ba2e9d055a928f8911ce5649e893813f +Subproject commit d2ad55a3460ad978788336d54449d6daf85514e2 From 2cd21d3b450807eeb4c6c764ec51627736f37b93 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 08:22:02 -0500 Subject: [PATCH 33/45] Try again. --- thirdparty/fastmcpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index d2ad55a3460a..3e91d1caa51f 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit d2ad55a3460ad978788336d54449d6daf85514e2 +Subproject commit 3e91d1caa51f14a6c8ef295f25dda58cc2c8aa07 From 929d1f602d07497043769c37ca384e2bc89e8b6f Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 08:33:36 -0500 Subject: [PATCH 34/45] Try again. --- thirdparty/fastmcpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index 3e91d1caa51f..8afd6a70bc9f 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit 3e91d1caa51f14a6c8ef295f25dda58cc2c8aa07 +Subproject commit 8afd6a70bc9f468b8941363f8b9290ce7ef80f31 From 3bf4efcc9e25f40346358c0b81442b357ca2ba37 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 09:16:36 -0500 Subject: [PATCH 35/45] Try again. --- CMakeLists.txt | 9 --------- source/MRMcp/CMakeLists.txt | 14 ++++++++------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a7c4ebb4d68c..92ad22643295 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -295,15 +295,6 @@ IF(NOT MR_EMSCRIPTEN AND NOT APPLE) ENDIF() IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) - # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. - IF(MESHLIB_USE_VCPKG OR APPLE) - IF(APPLE) - set(FASTMCPP_DEPS_ADD_SUBDIRECTORY ON) - ENDIF() - add_subdirectory(${MESHLIB_THIRDPARTY_DIR}/fastmcpp fastmcpp) - ELSE() - find_package(fastmcpp REQUIRED) - ENDIF() add_subdirectory(${PROJECT_SOURCE_DIR}/MRMcp ./MRMcp) ENDIF() diff --git a/source/MRMcp/CMakeLists.txt b/source/MRMcp/CMakeLists.txt index 44b4deea004d..cd2e8e4ccd8f 100644 --- a/source/MRMcp/CMakeLists.txt +++ b/source/MRMcp/CMakeLists.txt @@ -8,25 +8,27 @@ add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS}) target_link_libraries(${PROJECT_NAME} PUBLIC MRMesh - fastmcpp::fastmcpp_core ) IF(MR_PCH) target_precompile_headers(${PROJECT_NAME} REUSE_FROM MRPch) ENDIF() -# Fastmcpp stuff. -option(MESHLIB_BUILD_MCP "Enable MCP server" ON) -IF(MESHLIB_BUILD_MCP AND NOT MR_EMSCRIPTEN) +# On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. +IF(MESHLIB_USE_VCPKG OR APPLE) + IF(APPLE) + set(FASTMCPP_DEPS_ADD_SUBDIRECTORY ON) + ENDIF() + add_subdirectory(${MESHLIB_THIRDPARTY_DIR}/fastmcpp fastmcpp) target_link_libraries(${PROJECT_NAME} PRIVATE fastmcpp_core) - target_include_directories(${PROJECT_NAME} PRIVATE ${MESHLIB_THIRDPARTY_DIR}/fastmcpp/include ${MESHLIB_THIRDPARTY_DIR}/cpp-httplib ${MESHLIB_THIRDPARTY_DIR}/nlohmann-json/include ) ELSE() - target_compile_definitions(${PROJECT_NAME} PRIVATE MR_ENABLE_MCP_SERVER=0) + find_package(fastmcpp REQUIRED) + target_link_libraries(${PROJECT_NAME} PUBLIC fastmcpp::fastmcpp_core) ENDIF() install( From d8022ef53f1e5f98c4437a32a3a38c86f0159bf9 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 09:32:13 -0500 Subject: [PATCH 36/45] Try again. --- source/MRMcp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/MRMcp/CMakeLists.txt b/source/MRMcp/CMakeLists.txt index cd2e8e4ccd8f..bdfd08be2919 100644 --- a/source/MRMcp/CMakeLists.txt +++ b/source/MRMcp/CMakeLists.txt @@ -19,7 +19,7 @@ IF(MESHLIB_USE_VCPKG OR APPLE) IF(APPLE) set(FASTMCPP_DEPS_ADD_SUBDIRECTORY ON) ENDIF() - add_subdirectory(${MESHLIB_THIRDPARTY_DIR}/fastmcpp fastmcpp) + add_subdirectory(../fastmcpp fastmcpp) target_link_libraries(${PROJECT_NAME} PRIVATE fastmcpp_core) target_include_directories(${PROJECT_NAME} PRIVATE ${MESHLIB_THIRDPARTY_DIR}/fastmcpp/include From d657351f14de96d47f3d869877746d4a730f2526 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 10:01:15 -0500 Subject: [PATCH 37/45] Try again. --- thirdparty/fastmcpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirdparty/fastmcpp b/thirdparty/fastmcpp index 8afd6a70bc9f..be0a7f43f185 160000 --- a/thirdparty/fastmcpp +++ b/thirdparty/fastmcpp @@ -1 +1 @@ -Subproject commit 8afd6a70bc9f468b8941363f8b9290ce7ef80f31 +Subproject commit be0a7f43f185102cd488f62bf201eb3f4360663a From 8dc86d26f11b2a113077420cddc8e3380bb50920 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 10:29:01 -0500 Subject: [PATCH 38/45] Again? --- source/MRMcp/MRMcpConfig.cmake | 15 +++++++++++++++ source/MRViewer/MRViewerConfig.cmake | 14 ++------------ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 source/MRMcp/MRMcpConfig.cmake diff --git a/source/MRMcp/MRMcpConfig.cmake b/source/MRMcp/MRMcpConfig.cmake new file mode 100644 index 000000000000..ac90f8fdb4b8 --- /dev/null +++ b/source/MRMcp/MRMcpConfig.cmake @@ -0,0 +1,15 @@ +include(CMakeFindDependencyMacro) + +if(NOT EMSCRIPTEN) + find_dependency(glfw3) +endif() + +# static builds require to find private dependencies +if(EMSCRIPTEN) + find_dependency(Boost COMPONENTS locale) + find_dependency(glad) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/MRViewerTargets.cmake") + +check_required_components(MRViewer) diff --git a/source/MRViewer/MRViewerConfig.cmake b/source/MRViewer/MRViewerConfig.cmake index ac90f8fdb4b8..2ef41d17fded 100644 --- a/source/MRViewer/MRViewerConfig.cmake +++ b/source/MRViewer/MRViewerConfig.cmake @@ -1,15 +1,5 @@ include(CMakeFindDependencyMacro) -if(NOT EMSCRIPTEN) - find_dependency(glfw3) +IF(NOT MESHLIB_USE_VCPKG AND NOT APPLE) + find_dependency(fastmcpp) endif() - -# static builds require to find private dependencies -if(EMSCRIPTEN) - find_dependency(Boost COMPONENTS locale) - find_dependency(glad) -endif() - -include("${CMAKE_CURRENT_LIST_DIR}/MRViewerTargets.cmake") - -check_required_components(MRViewer) From c86744ed7b45fd7a4dacede8e8294050d0d30d30 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Thu, 16 Apr 2026 10:48:30 -0500 Subject: [PATCH 39/45] Wrong file. --- source/MRMcp/MRMcpConfig.cmake | 14 ++------------ source/MRViewer/MRViewerConfig.cmake | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/source/MRMcp/MRMcpConfig.cmake b/source/MRMcp/MRMcpConfig.cmake index ac90f8fdb4b8..2ef41d17fded 100644 --- a/source/MRMcp/MRMcpConfig.cmake +++ b/source/MRMcp/MRMcpConfig.cmake @@ -1,15 +1,5 @@ include(CMakeFindDependencyMacro) -if(NOT EMSCRIPTEN) - find_dependency(glfw3) +IF(NOT MESHLIB_USE_VCPKG AND NOT APPLE) + find_dependency(fastmcpp) endif() - -# static builds require to find private dependencies -if(EMSCRIPTEN) - find_dependency(Boost COMPONENTS locale) - find_dependency(glad) -endif() - -include("${CMAKE_CURRENT_LIST_DIR}/MRViewerTargets.cmake") - -check_required_components(MRViewer) diff --git a/source/MRViewer/MRViewerConfig.cmake b/source/MRViewer/MRViewerConfig.cmake index 2ef41d17fded..ac90f8fdb4b8 100644 --- a/source/MRViewer/MRViewerConfig.cmake +++ b/source/MRViewer/MRViewerConfig.cmake @@ -1,5 +1,15 @@ include(CMakeFindDependencyMacro) -IF(NOT MESHLIB_USE_VCPKG AND NOT APPLE) - find_dependency(fastmcpp) +if(NOT EMSCRIPTEN) + find_dependency(glfw3) endif() + +# static builds require to find private dependencies +if(EMSCRIPTEN) + find_dependency(Boost COMPONENTS locale) + find_dependency(glad) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/MRViewerTargets.cmake") + +check_required_components(MRViewer) From 54d3a72606d604cfd3d8ff44a3d5bcae1432b655 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Fri, 17 Apr 2026 04:09:44 -0500 Subject: [PATCH 40/45] Try again. --- source/MRMcp/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/MRMcp/CMakeLists.txt b/source/MRMcp/CMakeLists.txt index bdfd08be2919..9d117baeff9a 100644 --- a/source/MRMcp/CMakeLists.txt +++ b/source/MRMcp/CMakeLists.txt @@ -24,6 +24,8 @@ IF(MESHLIB_USE_VCPKG OR APPLE) target_include_directories(${PROJECT_NAME} PRIVATE ${MESHLIB_THIRDPARTY_DIR}/fastmcpp/include ${MESHLIB_THIRDPARTY_DIR}/cpp-httplib + ) + target_include_directories(${PROJECT_NAME} PUBLIC ${MESHLIB_THIRDPARTY_DIR}/nlohmann-json/include ) ELSE() From 77c9e71b1be328343b026e67582d744f73738f71 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Fri, 17 Apr 2026 04:20:26 -0500 Subject: [PATCH 41/45] Again. --- source/MRMcp/CMakeLists.txt | 2 -- source/MRViewer/CMakeLists.txt | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/source/MRMcp/CMakeLists.txt b/source/MRMcp/CMakeLists.txt index 9d117baeff9a..bdfd08be2919 100644 --- a/source/MRMcp/CMakeLists.txt +++ b/source/MRMcp/CMakeLists.txt @@ -24,8 +24,6 @@ IF(MESHLIB_USE_VCPKG OR APPLE) target_include_directories(${PROJECT_NAME} PRIVATE ${MESHLIB_THIRDPARTY_DIR}/fastmcpp/include ${MESHLIB_THIRDPARTY_DIR}/cpp-httplib - ) - target_include_directories(${PROJECT_NAME} PUBLIC ${MESHLIB_THIRDPARTY_DIR}/nlohmann-json/include ) ELSE() diff --git a/source/MRViewer/CMakeLists.txt b/source/MRViewer/CMakeLists.txt index eeec1aa916f8..39dd85983b8e 100644 --- a/source/MRViewer/CMakeLists.txt +++ b/source/MRViewer/CMakeLists.txt @@ -113,6 +113,13 @@ ENDIF() IF(MESHLIB_BUILD_MCP) target_link_libraries(${PROJECT_NAME} PRIVATE MRMcp) + + # If we're using fastmcpp from a subdirectory, we need to add this explicitly. Not the best way to do this, we should probably find a proper solution. + IF(MESHLIB_USE_VCPKG OR APPLE) + target_include_directories(${PROJECT_NAME} PRIVATE + ${MESHLIB_THIRDPARTY_DIR}/nlohmann-json/include + ) + ENDIF() ELSE() target_compile_definitions(${PROJECT_NAME} PRIVATE MESHLIB_NO_MCP) ENDIF() From aa7f3677081d1ca1735ebc6711d6a0ac828939e7 Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Fri, 17 Apr 2026 05:21:33 -0500 Subject: [PATCH 42/45] Try again. --- scripts/build_thirdparty.sh | 2 +- source/MRMcp/CMakeLists.txt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/build_thirdparty.sh b/scripts/build_thirdparty.sh index 645a25fabcdc..fa1a3c760a00 100755 --- a/scripts/build_thirdparty.sh +++ b/scripts/build_thirdparty.sh @@ -140,7 +140,7 @@ else # Skip this on Mac, we use `add_subdirectory()` for those libraries there. # This is because we can't use `find_package()` there to find our own libraries, because that breaks Python modules, as documented in the root `CMakeLists.txt`. - if [[ $OSTYPE == 'darwin'* ]]; then + if [[ $OSTYPE != 'darwin'* ]]; then # Build nlohmann-json separately. It is header-only, this just installs it. It is a dependency of fastmcpp. CMAKE_OPTIONS="${MR_CMAKE_OPTIONS}" ${SCRIPT_DIR}/thirdparty/nlohmann-json.sh "$MESHLIB_THIRDPARTY_DIR/nlohmann-json" # Build cpp-httplib separately. It is header-only, this just installs it. It is a dependency of fastmcpp. diff --git a/source/MRMcp/CMakeLists.txt b/source/MRMcp/CMakeLists.txt index bdfd08be2919..e88278e7e200 100644 --- a/source/MRMcp/CMakeLists.txt +++ b/source/MRMcp/CMakeLists.txt @@ -16,9 +16,7 @@ ENDIF() # On Windows, and on Linux+Vcpkg there is no third-party build script, so add fastmcpp as a subdirectory. IF(MESHLIB_USE_VCPKG OR APPLE) - IF(APPLE) - set(FASTMCPP_DEPS_ADD_SUBDIRECTORY ON) - ENDIF() + set(FASTMCPP_DEPS_ADD_SUBDIRECTORY ON) add_subdirectory(../fastmcpp fastmcpp) target_link_libraries(${PROJECT_NAME} PRIVATE fastmcpp_core) target_include_directories(${PROJECT_NAME} PRIVATE From dab25f97ac7b93640d085e96d5bfdecf0376444a Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Fri, 17 Apr 2026 07:17:34 -0500 Subject: [PATCH 43/45] Try again. --- CMakeLists.txt | 3 +++ docker/ubuntu22Dockerfile | 1 + docker/ubuntu24Dockerfile | 1 + 3 files changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 92ad22643295..69e33798056c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,9 @@ option(MESHLIB_BUILD_GENERATED_C_BINDINGS "Build C bindings (assuming they are a option(MESHLIB_BUILD_MRCUDA "Build MRCuda library" ON) option(MESHLIB_EXPERIMENTAL_HIP "(experimental) Use HIP toolkit for MRCuda library" OFF) +IF(MR_EMSCRIPTEN) + set(MESHLIB_BUILD_MCP OFF) +ENDIF() IF(MR_EMSCRIPTEN OR APPLE) set(MESHLIB_BUILD_MRCUDA OFF) ENDIF() diff --git a/docker/ubuntu22Dockerfile b/docker/ubuntu22Dockerfile index 2c9e7aa381e6..058a0ad4be60 100644 --- a/docker/ubuntu22Dockerfile +++ b/docker/ubuntu22Dockerfile @@ -44,6 +44,7 @@ COPY scripts/mrbind-pybind11/install_all_python_versions_ubuntu_pkgs.sh scripts/ COPY --from=build /home/MeshLib/lib /usr/local/lib/meshlib-thirdparty-lib/lib COPY --from=build /home/MeshLib/include /usr/local/lib/meshlib-thirdparty-lib/include +COPY --from=build /home/MeshLib/share /usr/local/lib/meshlib-thirdparty-lib/share ENV MR_STATE=DOCKER_BUILD diff --git a/docker/ubuntu24Dockerfile b/docker/ubuntu24Dockerfile index 892f0b599357..8e87c2630c65 100644 --- a/docker/ubuntu24Dockerfile +++ b/docker/ubuntu24Dockerfile @@ -52,6 +52,7 @@ COPY scripts/mrbind-pybind11/install_all_python_versions_ubuntu_pkgs.sh scripts/ COPY --from=build /home/MeshLib/lib /usr/local/lib/meshlib-thirdparty-lib/lib COPY --from=build /home/MeshLib/include /usr/local/lib/meshlib-thirdparty-lib/include +COPY --from=build /home/MeshLib/share /usr/local/lib/meshlib-thirdparty-lib/share COPY --from=cuda /usr/local/cuda-12.6 /usr/local/cuda-12.6 ENV MR_STATE=DOCKER_BUILD From 13db44b6e2b365e937a37fcdf87d5766ae303a4f Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Fri, 17 Apr 2026 07:59:55 -0500 Subject: [PATCH 44/45] Try again. --- .github/workflows/build-test-ubuntu-arm64.yml | 1 + .github/workflows/build-test-ubuntu-x64.yml | 1 + .github/workflows/update-docs-manual.yml | 3 ++- .gitpod.yml | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test-ubuntu-arm64.yml b/.github/workflows/build-test-ubuntu-arm64.yml index 1c908382c4bb..f470eb9a7e9c 100644 --- a/.github/workflows/build-test-ubuntu-arm64.yml +++ b/.github/workflows/build-test-ubuntu-arm64.yml @@ -99,6 +99,7 @@ jobs: run: | ln -s /usr/local/lib/meshlib-thirdparty-lib/lib ./lib ln -s /usr/local/lib/meshlib-thirdparty-lib/include ./include + ln -s /usr/local/lib/meshlib-thirdparty-lib/share ./share - name: Install MRBind if: ${{ inputs.mrbind || inputs.mrbind_c }} diff --git a/.github/workflows/build-test-ubuntu-x64.yml b/.github/workflows/build-test-ubuntu-x64.yml index 957c9d2dcfa6..ed20dc196c21 100644 --- a/.github/workflows/build-test-ubuntu-x64.yml +++ b/.github/workflows/build-test-ubuntu-x64.yml @@ -78,6 +78,7 @@ jobs: run: | ln -s /usr/local/lib/meshlib-thirdparty-lib/lib ./lib ln -s /usr/local/lib/meshlib-thirdparty-lib/include ./include + ln -s /usr/local/lib/meshlib-thirdparty-lib/share ./share - name: Install MRBind if: ${{ inputs.mrbind || inputs.mrbind_c }} diff --git a/.github/workflows/update-docs-manual.yml b/.github/workflows/update-docs-manual.yml index 1862b36ee64e..a8b9be47c0de 100644 --- a/.github/workflows/update-docs-manual.yml +++ b/.github/workflows/update-docs-manual.yml @@ -47,11 +47,12 @@ jobs: export HOME=${RUNNER_TEMP} git config --global --add safe.directory '*' git submodule update --init --recursive --depth 1 thirdparty/imgui thirdparty/eigen thirdparty/parallel-hashmap thirdparty/mrbind-pybind11 thirdparty/mrbind - + - name: Install thirdparty libs run: | ln -s /usr/local/lib/meshlib-thirdparty-lib/lib ./lib ln -s /usr/local/lib/meshlib-thirdparty-lib/include ./include + ln -s /usr/local/lib/meshlib-thirdparty-lib/share ./share - name: Install mrbind run: scripts/mrbind/install_mrbind_ubuntu.sh diff --git a/.gitpod.yml b/.gitpod.yml index 766c01e8be62..dc01eb0def10 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -9,3 +9,4 @@ tasks: init: | ln -s /usr/local/lib/meshlib-thirdparty-lib/lib ./lib ln -s /usr/local/lib/meshlib-thirdparty-lib/include ./include + ln -s /usr/local/lib/meshlib-thirdparty-lib/share ./share From 4ff2cd3203d0809663afac5f96302063f602e1aa Mon Sep 17 00:00:00 2001 From: Egor Mikhaylov Date: Fri, 17 Apr 2026 09:07:46 -0500 Subject: [PATCH 45/45] Fix VS builds. --- source/MRMcp/MRMcp.vcxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/MRMcp/MRMcp.vcxproj b/source/MRMcp/MRMcp.vcxproj index b6686e94bb58..f612bac12ec6 100644 --- a/source/MRMcp/MRMcp.vcxproj +++ b/source/MRMcp/MRMcp.vcxproj @@ -33,12 +33,12 @@ - StaticLibrary + DynamicLibrary true Unicode - StaticLibrary + DynamicLibrary false false Unicode