diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index c19c154622810fcf7821d7137e29ed48632a0b15..bd9e4c2d824f33707c058237662532e3ef5d72a2 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -638,6 +638,7 @@ set(SRCS
   cmMathCommand.h
   cmMessageCommand.cxx
   cmMessageCommand.h
+  cmMessageMetadata.h
   cmOptionCommand.cxx
   cmOptionCommand.h
   cmOutputRequiredFilesCommand.cxx
diff --git a/Source/CTest/cmCTestBuildAndTestHandler.cxx b/Source/CTest/cmCTestBuildAndTestHandler.cxx
index a18cbb49bcafefb0403aa1548f908f1e415397ed..adfc8ef774f96536431b9ce143abc76659999af5 100644
--- a/Source/CTest/cmCTestBuildAndTestHandler.cxx
+++ b/Source/CTest/cmCTestBuildAndTestHandler.cxx
@@ -19,6 +19,8 @@
 #include "cmWorkingDirectory.h"
 #include "cmake.h"
 
+struct cmMessageMetadata;
+
 cmCTestBuildAndTestHandler::cmCTestBuildAndTestHandler()
 {
   this->BuildTwoConfig = false;
@@ -125,7 +127,7 @@ public:
     : CM(cm)
   {
     cmSystemTools::SetMessageCallback(
-      [&s](const std::string& msg, const char* /*unused*/) {
+      [&s](const std::string& msg, const cmMessageMetadata& /* unused */) {
         s += msg;
         s += "\n";
       });
diff --git a/Source/CursesDialog/ccmake.cxx b/Source/CursesDialog/ccmake.cxx
index 85e256b8bc3d5668b00196019e234e303e1cafc3..1ba45e5f7a13f0b31802c37a2a1a1482a3058e1b 100644
--- a/Source/CursesDialog/ccmake.cxx
+++ b/Source/CursesDialog/ccmake.cxx
@@ -19,6 +19,7 @@
 #include "cmCursesStandardIncludes.h"
 #include "cmDocumentation.h"
 #include "cmDocumentationEntry.h" // IWYU pragma: keep
+#include "cmMessageMetadata.h"
 #include "cmState.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
@@ -181,8 +182,8 @@ int main(int argc, char const* const* argv)
     return msg;
   };
   cmSystemTools::SetMessageCallback(
-    [&](const std::string& message, const char* title) {
-      myform->AddError(cleanMessage(message), title);
+    [&](const std::string& message, const cmMessageMetadata& md) {
+      myform->AddError(cleanMessage(message), md.title);
     });
   cmSystemTools::SetStderrCallback([&](const std::string& message) {
     myform->AddError(cleanMessage(message), "");
diff --git a/Source/QtDialog/QCMake.cxx b/Source/QtDialog/QCMake.cxx
index a83622afe87046f15107b12d2dddf37464440960..e6faef47033e5fa73ed3f88abd3a15396d0278ac 100644
--- a/Source/QtDialog/QCMake.cxx
+++ b/Source/QtDialog/QCMake.cxx
@@ -13,6 +13,7 @@
 
 #include "cmExternalMakefileProjectGenerator.h"
 #include "cmGlobalGenerator.h"
+#include "cmMessageMetadata.h"
 #include "cmState.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
@@ -37,8 +38,8 @@ QCMake::QCMake(QObject* p)
   cmSystemTools::SetRunCommandHideConsole(true);
 
   cmSystemTools::SetMessageCallback(
-    [this](std::string const& msg, const char* title) {
-      this->messageCallback(msg, title);
+    [this](std::string const& msg, const cmMessageMetadata& md) {
+      this->messageCallback(msg, md.title);
     });
   cmSystemTools::SetStdoutCallback(
     [this](std::string const& msg) { this->stdoutCallback(msg); });
diff --git a/Source/cmMessageMetadata.h b/Source/cmMessageMetadata.h
new file mode 100644
index 0000000000000000000000000000000000000000..5688dc57a2df3b0a66e049ae335f8a2b0560303e
--- /dev/null
+++ b/Source/cmMessageMetadata.h
@@ -0,0 +1,8 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+struct cmMessageMetadata
+{
+  const char* title = nullptr;
+};
diff --git a/Source/cmMessenger.cxx b/Source/cmMessenger.cxx
index af83478a65bf904e64bb5f47ac566ade6140666e..4c0faa9236b9a5f17605631272e06c6f7333a615 100644
--- a/Source/cmMessenger.cxx
+++ b/Source/cmMessenger.cxx
@@ -3,6 +3,7 @@
 #include "cmMessenger.h"
 
 #include "cmDocumentationFormatter.h"
+#include "cmMessageMetadata.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 
@@ -120,12 +121,15 @@ void displayMessage(MessageType t, std::ostringstream& msg)
 #endif
 
   // Output the message.
+  cmMessageMetadata md;
   if (t == MessageType::FATAL_ERROR || t == MessageType::INTERNAL_ERROR ||
       t == MessageType::DEPRECATION_ERROR || t == MessageType::AUTHOR_ERROR) {
     cmSystemTools::SetErrorOccured();
-    cmSystemTools::Message(msg.str(), "Error");
+    md.title = "Error";
+    cmSystemTools::Message(msg.str(), md);
   } else {
-    cmSystemTools::Message(msg.str(), "Warning");
+    md.title = "Warning";
+    cmSystemTools::Message(msg.str(), md);
   }
 }
 
diff --git a/Source/cmSystemTools.cxx b/Source/cmSystemTools.cxx
index 2fba13f436b7adcc0d69c8e1806520fff9ec2b52..9b81bf2675bd8e6785b8ebfb37c42d8e7d07913e 100644
--- a/Source/cmSystemTools.cxx
+++ b/Source/cmSystemTools.cxx
@@ -19,6 +19,7 @@
 #include <cm3p/uv.h>
 
 #include "cmDuration.h"
+#include "cmMessageMetadata.h"
 #include "cmProcessOutput.h"
 #include "cmRange.h"
 #include "cmStringAlgorithms.h"
@@ -262,9 +263,16 @@ void cmSystemTools::Stdout(const std::string& s)
 }
 
 void cmSystemTools::Message(const std::string& m, const char* title)
+{
+  cmMessageMetadata md;
+  md.title = title;
+  Message(m, md);
+}
+
+void cmSystemTools::Message(const std::string& m, const cmMessageMetadata& md)
 {
   if (s_MessageCallback) {
-    s_MessageCallback(m, title);
+    s_MessageCallback(m, md);
   } else {
     std::cerr << m << std::endl;
   }
diff --git a/Source/cmSystemTools.h b/Source/cmSystemTools.h
index 474f591ef551a4e5866410df3d271863b2b4a52e..5c3b5a9a0a9365e02d3d6f132632a19ed54304ff 100644
--- a/Source/cmSystemTools.h
+++ b/Source/cmSystemTools.h
@@ -19,6 +19,8 @@
 #include "cmDuration.h"
 #include "cmProcessOutput.h"
 
+struct cmMessageMetadata;
+
 /** \class cmSystemTools
  * \brief A collection of useful functions for CMake.
  *
@@ -40,7 +42,8 @@ public:
   /** Map help document name to file name.  */
   static std::string HelpFileName(cm::string_view);
 
-  using MessageCallback = std::function<void(const std::string&, const char*)>;
+  using MessageCallback =
+    std::function<void(const std::string&, const cmMessageMetadata&)>;
   /**
    *  Set the function used by GUIs to display error messages
    *  Function gets passed: message as a const char*,
@@ -57,6 +60,7 @@ public:
    * Display a message.
    */
   static void Message(const std::string& m, const char* title = nullptr);
+  static void Message(const std::string& m, const cmMessageMetadata& md);
 
   using OutputCallback = std::function<void(std::string const&)>;
 
diff --git a/Source/cmakemain.cxx b/Source/cmakemain.cxx
index ad648186bd5fef0699bd7b472173cb9b6200cc95..60ac0caa726869820c5826406a5173cf8f7f38c6 100644
--- a/Source/cmakemain.cxx
+++ b/Source/cmakemain.cxx
@@ -38,6 +38,8 @@
 
 #include "cmsys/Encoding.hxx"
 
+struct cmMessageMetadata;
+
 namespace {
 #ifndef CMAKE_BOOTSTRAP
 const char* cmDocumentationName[][2] = {
@@ -147,8 +149,8 @@ std::string cmakemainGetStack(cmake* cm)
   return msg;
 }
 
-void cmakemainMessageCallback(const std::string& m, const char* /*unused*/,
-                              cmake* cm)
+void cmakemainMessageCallback(const std::string& m,
+                              const cmMessageMetadata& /* unused */, cmake* cm)
 {
   std::cerr << m << cmakemainGetStack(cm) << std::endl;
 }
@@ -342,8 +344,8 @@ int do_cmake(int ac, char const* const* av)
   cm.SetHomeDirectory("");
   cm.SetHomeOutputDirectory("");
   cmSystemTools::SetMessageCallback(
-    [&cm](const std::string& msg, const char* title) {
-      cmakemainMessageCallback(msg, title, &cm);
+    [&cm](const std::string& msg, const cmMessageMetadata& md) {
+      cmakemainMessageCallback(msg, md, &cm);
     });
   cm.SetProgressCallback([&cm](const std::string& msg, float prog) {
     cmakemainProgressCallback(msg, prog, &cm);
@@ -624,8 +626,8 @@ int do_build(int ac, char const* const* av)
 
   cmake cm(cmake::RoleInternal, cmState::Project);
   cmSystemTools::SetMessageCallback(
-    [&cm](const std::string& msg, const char* title) {
-      cmakemainMessageCallback(msg, title, &cm);
+    [&cm](const std::string& msg, const cmMessageMetadata& md) {
+      cmakemainMessageCallback(msg, md, &cm);
     });
   cm.SetProgressCallback([&cm](const std::string& msg, float prog) {
     cmakemainProgressCallback(msg, prog, &cm);
@@ -857,8 +859,8 @@ int do_install(int ac, char const* const* av)
   cmake cm(cmake::RoleScript, cmState::Script);
 
   cmSystemTools::SetMessageCallback(
-    [&cm](const std::string& msg, const char* title) {
-      cmakemainMessageCallback(msg, title, &cm);
+    [&cm](const std::string& msg, const cmMessageMetadata& md) {
+      cmakemainMessageCallback(msg, md, &cm);
     });
   cm.SetProgressCallback([&cm](const std::string& msg, float prog) {
     cmakemainProgressCallback(msg, prog, &cm);
@@ -938,8 +940,8 @@ int do_open(int ac, char const* const* av)
 
   cmake cm(cmake::RoleInternal, cmState::Unknown);
   cmSystemTools::SetMessageCallback(
-    [&cm](const std::string& msg, const char* title) {
-      cmakemainMessageCallback(msg, title, &cm);
+    [&cm](const std::string& msg, const cmMessageMetadata& md) {
+      cmakemainMessageCallback(msg, md, &cm);
     });
   cm.SetProgressCallback([&cm](const std::string& msg, float prog) {
     cmakemainProgressCallback(msg, prog, &cm);