diff --git a/framework/rcommand/CMakeLists.txt b/framework/rcommand/CMakeLists.txt index c7d8b2efa3..dfb5d399f3 100644 --- a/framework/rcommand/CMakeLists.txt +++ b/framework/rcommand/CMakeLists.txt @@ -23,12 +23,15 @@ muse_create_module(muse_rcommand ALIAS muse::rcommand) target_sources(muse_rcommand PRIVATE rcommandmodule.cpp rcommandmodule.h - ircommanddispatcher.h - rcommandable.h - rcommandtypes.h + icommanddispatcher.h + imodulecommands.h + commandable.h + commandtypes.h - internal/rcommanddispatcher.cpp - internal/rcommanddispatcher.h + internal/commanddispatcher.cpp + internal/commanddispatcher.h + internal/commandsregister.cpp + internal/commandsregister.h ) #if (MUSE_MODULE_RCOMMAND_TESTS) diff --git a/framework/rcommand/rcommandable.h b/framework/rcommand/commandable.h similarity index 74% rename from framework/rcommand/rcommandable.h rename to framework/rcommand/commandable.h index cfb175bbd2..9025afca04 100644 --- a/framework/rcommand/rcommandable.h +++ b/framework/rcommand/commandable.h @@ -2,7 +2,7 @@ * SPDX-License-Identifier: GPL-3.0-only * MuseScore/Audacity CLA applies * - * Copyright (C) 2026 MuseScore/Audacity and others + * Copyright (C) MuseScore/Audacity and others * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -19,31 +19,31 @@ #pragma once -#include "ircommanddispatcher.h" +#include "icommanddispatcher.h" namespace muse::rcommand { -class RCommandable +class Commandable { public: - virtual ~RCommandable() + virtual ~Commandable() { if (m_dispatcher) { m_dispatcher->unreg(this); } } - inline void setDispatcher(IRCommandDispatcher* dispatcher) + inline void setDispatcher(ICommandDispatcher* dispatcher) { m_dispatcher = dispatcher; } - inline bool isDispatcher(const IRCommandDispatcher* dispatcher) const + inline bool isDispatcher(const ICommandDispatcher* dispatcher) const { return m_dispatcher == dispatcher; } private: - IRCommandDispatcher* m_dispatcher = nullptr; + ICommandDispatcher* m_dispatcher = nullptr; }; } diff --git a/framework/rcommand/rcommandtypes.h b/framework/rcommand/commandtypes.h similarity index 56% rename from framework/rcommand/rcommandtypes.h rename to framework/rcommand/commandtypes.h index ee289a34ff..57b5cf68b4 100644 --- a/framework/rcommand/rcommandtypes.h +++ b/framework/rcommand/commandtypes.h @@ -2,7 +2,7 @@ * SPDX-License-Identifier: GPL-3.0-only * MuseScore/Audacity CLA applies * - * Copyright (C) 2026 MuseScore/Audacity and others + * Copyright (C) MuseScore/Audacity and others * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -20,13 +20,77 @@ #pragma once #include +#include +#include #include "global/types/uri.h" #include "global/types/ret.h" +#include "global/types/string.h" +#include "global/types/val.h" +#include "global/types/mnemonicstring.h" +#include "global/types/translatablestring.h" +#include "global/types/color.h" +#include "ui/uitypes.h" namespace muse::rcommand { +constexpr std::string_view COMMAND_SCHEME = "command://"; using Command = Uri; using CommandQuery = UriQuery; + +// Info + +enum class DataType { + Undefined = 0, + String, + Integer, + Float, + Boolean, + Object, + Array, + Null +}; + +struct Arg { + DataType type = DataType::Undefined; + String description; + Val min; + Val max; +}; + +struct InputSchema { + std::map args; +}; + +enum class Checkable { + No = 0, + Yes +}; + +struct Decoration { + ui::IconCode::Code iconCode = ui::IconCode::Code::NONE; + Color iconColor; + Checkable checkable = Checkable::No; + + Decoration() = default; + Decoration(ui::IconCode::Code iconCode) + : iconCode(iconCode) {} + Decoration(ui::IconCode::Code iconCode, Color iconColor) + : iconCode(iconCode), iconColor(iconColor) {} + Decoration(ui::IconCode::Code iconCode, Color iconColor, Checkable checkable) + : iconCode(iconCode), iconColor(iconColor), checkable(checkable) {} +}; + +struct CommandInfo +{ + Command command; + MnemonicString title; + TranslatableString description; + InputSchema inputSchema; + Decoration decoration; +}; + +// Call + using CallId = uint64_t; struct Request diff --git a/framework/rcommand/ircommanddispatcher.h b/framework/rcommand/icommanddispatcher.h similarity index 73% rename from framework/rcommand/ircommanddispatcher.h rename to framework/rcommand/icommanddispatcher.h index cbebe55cdb..28ac162d2f 100644 --- a/framework/rcommand/ircommanddispatcher.h +++ b/framework/rcommand/icommanddispatcher.h @@ -2,7 +2,7 @@ * SPDX-License-Identifier: GPL-3.0-only * MuseScore/Audacity CLA applies * - * Copyright (C) 2026 MuseScore/Audacity and others + * Copyright (C) MuseScore/Audacity and others * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -23,21 +23,21 @@ #include "global/async/promise.h" -#include "rcommandtypes.h" +#include "commandtypes.h" namespace muse::rcommand { -class RCommandable; -class IRCommandDispatcher : MODULE_CONTEXT_INTERFACE +class Commandable; +class ICommandDispatcher : MODULE_CONTEXT_INTERFACE { - INTERFACE_ID(IRCommandDispatcher) + INTERFACE_ID(ICommandDispatcher) public: - virtual ~IRCommandDispatcher() = default; + virtual ~ICommandDispatcher() = default; using CallBack = std::function; virtual async::Promise dispatch(const Request& request) = 0; - virtual void onRequest(RCommandable* client, const Command& command, const CallBack& callback) = 0; - virtual void unreg(RCommandable* client) = 0; + virtual void onRequest(Commandable* client, const Command& command, const CallBack& callback) = 0; + virtual void unreg(Commandable* client) = 0; async::Promise dispatch(const CommandQuery& query) { diff --git a/framework/rcommand/icommandsregister.h b/framework/rcommand/icommandsregister.h new file mode 100644 index 0000000000..4340d5df0e --- /dev/null +++ b/framework/rcommand/icommandsregister.h @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore/Audacity CLA applies + * + * Copyright (C) MuseScore/Audacity and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "modularity/imoduleinterface.h" +#include "imodulecommands.h" + +namespace muse::rcommand { +class ICommandsRegister : MODULE_GLOBAL_INTERFACE +{ + INTERFACE_ID(ICommandsRegister) +public: + virtual ~ICommandsRegister() = default; + + virtual void reg(const IModuleCommandsPtr& module) = 0; + virtual void unreg(const IModuleCommandsPtr& module) = 0; + + virtual std::vector commandList() const = 0; +}; +} diff --git a/framework/rcommand/imodulecommands.h b/framework/rcommand/imodulecommands.h new file mode 100644 index 0000000000..f3a61991d4 --- /dev/null +++ b/framework/rcommand/imodulecommands.h @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore/Audacity CLA applies + * + * Copyright (C) MuseScore/Audacity and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "commandtypes.h" + +namespace muse::rcommand { +class IModuleCommands +{ +public: + + virtual ~IModuleCommands() = default; + + virtual std::string moduleName() const = 0; + virtual const std::vector& commandInfos() const = 0; +}; + +using IModuleCommandsPtr = std::shared_ptr; +} diff --git a/framework/rcommand/internal/rcommanddispatcher.cpp b/framework/rcommand/internal/commanddispatcher.cpp similarity index 75% rename from framework/rcommand/internal/rcommanddispatcher.cpp rename to framework/rcommand/internal/commanddispatcher.cpp index afe98de58a..35380bcfeb 100644 --- a/framework/rcommand/internal/rcommanddispatcher.cpp +++ b/framework/rcommand/internal/commanddispatcher.cpp @@ -16,19 +16,31 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#include "rcommanddispatcher.h" +#include "commanddispatcher.h" #include "async/promise.h" -#include "../rcommandable.h" -#include "../rcommandtypes.h" +#include "../commandable.h" +#include "../commandtypes.h" #include "log.h" using namespace muse; using namespace muse::rcommand; -async::Promise RCommandDispatcher::dispatch(const Request& request) +CommandDispatcher::~CommandDispatcher() +{ + for (auto it = m_clients.begin(); it != m_clients.end(); ++it) { + Commandable* client = it->second.client; + if (client) { + client->setDispatcher(nullptr); + } + } + + m_clients.clear(); +} + +async::Promise CommandDispatcher::dispatch(const Request& request) { return async::make_promise([this, request](auto resolve) { auto it = m_clients.find(Command(request.query.uri())); @@ -41,7 +53,7 @@ async::Promise RCommandDispatcher::dispatch(const Request& request) }); } -void RCommandDispatcher::onRequest(RCommandable* client, const Command& command, const CallBack& callback) +void CommandDispatcher::onRequest(Commandable* client, const Command& command, const CallBack& callback) { IF_ASSERT_FAILED(m_clients.find(command) == m_clients.end()) { LOGW() << "command already registered: " << command; @@ -52,7 +64,7 @@ void RCommandDispatcher::onRequest(RCommandable* client, const Command& command, client->setDispatcher(this); } -void RCommandDispatcher::unreg(RCommandable* client) +void CommandDispatcher::unreg(Commandable* client) { if (!client || !client->isDispatcher(this)) { return; diff --git a/framework/rcommand/internal/rcommanddispatcher.h b/framework/rcommand/internal/commanddispatcher.h similarity index 73% rename from framework/rcommand/internal/rcommanddispatcher.h rename to framework/rcommand/internal/commanddispatcher.h index 485c119829..ad4e063fd3 100644 --- a/framework/rcommand/internal/rcommanddispatcher.h +++ b/framework/rcommand/internal/commanddispatcher.h @@ -18,23 +18,24 @@ */ #pragma once -#include "../ircommanddispatcher.h" +#include "../icommanddispatcher.h" namespace muse::rcommand { -class RCommandDispatcher : public IRCommandDispatcher +class CommandDispatcher : public ICommandDispatcher { public: - RCommandDispatcher() = default; + CommandDispatcher() = default; + ~CommandDispatcher() override; async::Promise dispatch(const Request& request) override; - void onRequest(RCommandable* client, const Command& command, const CallBack& callback) override; - void unreg(RCommandable* client) override; + void onRequest(Commandable* client, const Command& command, const CallBack& callback) override; + void unreg(Commandable* client) override; private: struct Client { - RCommandable* client = nullptr; + Commandable* client = nullptr; CallBack callback = nullptr; }; diff --git a/framework/rcommand/internal/commandsregister.cpp b/framework/rcommand/internal/commandsregister.cpp new file mode 100644 index 0000000000..1c040d4390 --- /dev/null +++ b/framework/rcommand/internal/commandsregister.cpp @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore/Audacity CLA applies + * + * Copyright (C) 2026 MuseScore/Audacity and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "commandsregister.h" + +#include "log.h" + +using namespace muse; +using namespace muse::rcommand; + +void CommandsRegister::reg(const IModuleCommandsPtr& module) +{ + IF_ASSERT_FAILED(module) { + return; + } + + const std::string& moduleName = module->moduleName(); + IF_ASSERT_FAILED(!moduleName.empty()) { + return; + } + + IF_ASSERT_FAILED(m_modules.find(moduleName) == m_modules.end()) { + LOGW() << "module already registered: " << moduleName; + return; + } + + m_modules[moduleName] = module; +} + +void CommandsRegister::unreg(const IModuleCommandsPtr& module) +{ + IF_ASSERT_FAILED(module) { + return; + } + + const std::string& moduleName = module->moduleName(); + IF_ASSERT_FAILED(!moduleName.empty()) { + return; + } + + m_modules.erase(moduleName); +} + +std::vector CommandsRegister::commandList() const +{ + std::vector commands; + for (const auto& module : m_modules) { + const auto& infos = module.second->commandInfos(); + commands.insert(commands.end(), infos.begin(), infos.end()); + } + return commands; +} diff --git a/framework/rcommand/internal/commandsregister.h b/framework/rcommand/internal/commandsregister.h new file mode 100644 index 0000000000..10665b8855 --- /dev/null +++ b/framework/rcommand/internal/commandsregister.h @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore/Audacity CLA applies + * + * Copyright (C) 2026 MuseScore/Audacity and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #pragma once + + #include + #include + + #include "../icommandsregister.h" + +namespace muse::rcommand { +class CommandsRegister : public ICommandsRegister +{ +public: + CommandsRegister() = default; + + void reg(const IModuleCommandsPtr& module) override; + void unreg(const IModuleCommandsPtr& module) override; + std::vector commandList() const override; + +private: + std::map m_modules; +}; +} diff --git a/framework/rcommand/rcommandmodule.cpp b/framework/rcommand/rcommandmodule.cpp index 1d4ec23481..fb068a858e 100644 --- a/framework/rcommand/rcommandmodule.cpp +++ b/framework/rcommand/rcommandmodule.cpp @@ -2,7 +2,7 @@ * SPDX-License-Identifier: GPL-3.0-only * MuseScore/Audacity CLA applies * - * Copyright (C) 2026 MuseScore/Audacity and others + * Copyright (C) MuseScore/Audacity and others * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -19,7 +19,8 @@ #include "rcommandmodule.h" -#include "internal/rcommanddispatcher.h" +#include "internal/commandsregister.h" +#include "internal/commanddispatcher.h" using namespace muse; using namespace muse::rcommand; @@ -31,6 +32,11 @@ std::string RCommandModule::moduleName() const return mname; } +void RCommandModule::registerExports() +{ + globalIoc()->registerExport(mname, new CommandsRegister()); +} + modularity::IContextSetup* RCommandModule::newContext(const muse::modularity::ContextPtr& ctx) const { return new RCommandContext(ctx); @@ -38,5 +44,5 @@ modularity::IContextSetup* RCommandModule::newContext(const muse::modularity::Co void RCommandContext::registerExports() { - ioc()->registerExport(mname, new RCommandDispatcher()); + ioc()->registerExport(mname, new CommandDispatcher()); } diff --git a/framework/rcommand/rcommandmodule.h b/framework/rcommand/rcommandmodule.h index bfa60ec9dd..71386c2147 100644 --- a/framework/rcommand/rcommandmodule.h +++ b/framework/rcommand/rcommandmodule.h @@ -2,7 +2,7 @@ * SPDX-License-Identifier: GPL-3.0-only * MuseScore/Audacity CLA applies * - * Copyright (C) 2026 MuseScore/Audacity and others + * Copyright (C) MuseScore/Audacity and others * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -27,6 +27,8 @@ class RCommandModule : public modularity::IModuleSetup public: std::string moduleName() const override; + void registerExports() override; + modularity::IContextSetup* newContext(const muse::modularity::ContextPtr& ctx) const override; }; diff --git a/framework/rcontrol/mcp/mcpcontroller.cpp b/framework/rcontrol/mcp/mcpcontroller.cpp index e643cb22a3..f4aa96caf1 100644 --- a/framework/rcontrol/mcp/mcpcontroller.cpp +++ b/framework/rcontrol/mcp/mcpcontroller.cpp @@ -21,9 +21,13 @@ #include "mcpserver.h" +#include "global/stringutils.h" + #include "log.h" +#include "thirdparty/kors_logger/src/log_base.h" using namespace muse::rcontrol::mcp; +using namespace muse::rcommand; McpController::McpController(const modularity::ContextPtr& iocCtx) : Contextable(iocCtx) @@ -35,6 +39,27 @@ McpController::~McpController() deinit(); } +//! NOTE The tool name must be in the format: group_name +static std::string commandToToolName(const Command& command) +{ + std::string path = command.path(); + muse::strings::replace(path, "/", "_"); + return path; +} + +static CommandQuery commandQuery(const std::string& name, const JsonObject& args) +{ + UNUSED(args); // TODO: implement + std::string path = name; + muse::strings::replace(path, "_", "/"); + Command cmd(std::string(COMMAND_SCHEME), path); + CommandQuery q(cmd); + // for (const auto& arg : args) { + // q.set(arg.first, arg.second.toString()); + // } + return q; +} + void McpController::init() { m_mcpServer = std::make_unique(application()->version().toStdString()); @@ -50,7 +75,7 @@ void McpController::init() { LOGDA() << "Tools call: " << name; - dispatcher()->dispatch(name); + commandsDispatcher()->dispatch(commandQuery(name, args)); onResult(ToolResult()); }); @@ -68,9 +93,24 @@ void McpController::deinit() std::vector McpController::makeToolsList() const { - //! NOTE There will be an adapter to the RCommand infrastructure. std::vector tools; - tools.push_back({ String(u"play"), String(u"Play"), String(u"Start playback of the current score"), InputSchema() }); - tools.push_back({ String(u"stop"), String(u"Stop"), String(u"Stop playback of the current score"), InputSchema() }); + auto commandList = commandsRegister()->commandList(); + tools.reserve(commandList.size()); + for (const auto& info : commandList) { + Tool tool; + tool.name = commandToToolName(info.command); + tool.title = info.title.raw().translated().toStdString(); + tool.description = info.description.translated().toStdString(); + tool.inputSchema = InputSchema(); + // for (const auto& arg : info.inputSchema.args) { + // Property property; + // property.name = String::fromStdString(arg.first); + // property.type = String::fromStdString(arg.second.type); + // property.description = String::fromStdString(arg.second.description); + // property.minimum = String::fromStdString(arg.second.minimum); + // property.maximum = String::fromStdString(arg.second.maximum); + // } + tools.push_back(std::move(tool)); + } return tools; } diff --git a/framework/rcontrol/mcp/mcpcontroller.h b/framework/rcontrol/mcp/mcpcontroller.h index 70e174cf8e..9d4a504b41 100644 --- a/framework/rcontrol/mcp/mcpcontroller.h +++ b/framework/rcontrol/mcp/mcpcontroller.h @@ -23,7 +23,8 @@ #include "modularity/ioc.h" #include "global/iapplication.h" -#include "actions/iactionsdispatcher.h" +#include "rcommand/icommanddispatcher.h" +#include "rcommand/icommandsregister.h" #include "mcptypes.h" @@ -32,7 +33,8 @@ class McpServer; class McpController : public Contextable { GlobalInject application; - ContextInject dispatcher = { this }; + GlobalInject commandsRegister; + ContextInject commandsDispatcher = { this }; public: McpController(const modularity::ContextPtr& iocCtx); diff --git a/framework/rcontrol/mcp/mcptypes.h b/framework/rcontrol/mcp/mcptypes.h index fbf761055c..806d9e9ef7 100644 --- a/framework/rcontrol/mcp/mcptypes.h +++ b/framework/rcontrol/mcp/mcptypes.h @@ -21,7 +21,6 @@ #include -#include "global/types/string.h" #include "global/serialization/json.h" namespace muse::rcontrol::mcp { @@ -53,7 +52,7 @@ inline std::string to_string(DataType type) struct Property { DataType type; - String description; + std::string description; JsonValue minimum; JsonValue maximum; }; @@ -64,9 +63,9 @@ struct InputSchema { }; struct Tool { - String name; - String title; - String description; + std::string name; + std::string title; + std::string description; InputSchema inputSchema; };