Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore/Audacity CLA applies
4 : *
5 : * Copyright (C) 2026 MuseScore/Audacity and others
6 : *
7 : * This program is free software: you can redistribute it and/or modify
8 : * it under the terms of the GNU General Public License version 3 as
9 : * published by the Free Software Foundation.
10 : *
11 : * This program is distributed in the hope that it will be useful,
12 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : * GNU General Public License for more details.
15 : *
16 : * You should have received a copy of the GNU General Public License
17 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 : */
19 :
20 : #include "mcpserver.h"
21 :
22 : #include "serialization/json.h"
23 :
24 : #include "transport/tcptransport.h"
25 : #include "transport/httptransport.h"
26 :
27 : #include "log.h"
28 :
29 : static const char* PROTOCOL_VERSION = "2025-11-25";
30 : static const char* SERVER_NAME = "MuseMCP";
31 :
32 : using namespace muse::rcontrol::mcp;
33 :
34 0 : static std::map<std::string, muse::Val> toArgs(const muse::JsonObject& obj)
35 : {
36 0 : std::map<std::string, muse::Val> args;
37 0 : for (const auto& key : obj.keys()) {
38 0 : args[key] = obj[key].toVal();
39 0 : }
40 0 : return args;
41 0 : }
42 :
43 0 : McpServer::McpServer(const std::string& version, ITransport* transport)
44 0 : : m_version(version),
45 0 : m_transport(transport)
46 : {
47 0 : if (!m_transport) {
48 0 : m_transport = new HttpTransport();
49 : }
50 0 : }
51 :
52 0 : McpServer::~McpServer()
53 : {
54 0 : deinit();
55 0 : delete m_transport;
56 0 : m_transport = nullptr;
57 0 : }
58 :
59 0 : void McpServer::init()
60 : {
61 0 : IF_ASSERT_FAILED(m_transport) {
62 0 : return;
63 : }
64 :
65 0 : m_transport->onRequest([this](const ByteArray& request, const ITransport::ResponseHandler& onResponse) {
66 0 : JsonObject req = JsonDocument::fromJson(request).rootObject();
67 0 : onRequest(req, [onResponse](const JsonObject& res) {
68 0 : JsonDocument doc(res);
69 0 : onResponse(doc.toJson(JsonDocument::Format::Compact));
70 0 : });
71 0 : });
72 :
73 0 : m_transport->start();
74 : }
75 :
76 0 : void McpServer::deinit()
77 : {
78 0 : m_transport->stop();
79 0 : }
80 :
81 0 : void McpServer::onRequest(const JsonObject& request, const std::function<void(const JsonObject&)>& onResponse)
82 : {
83 0 : const String method = request.value("method").toString();
84 0 : if (method == u"initialize") {
85 0 : onInitialize(request, onResponse);
86 0 : } else if (method == u"tools/list") {
87 0 : onToolsList(request, onResponse);
88 0 : } else if (method == u"tools/call") {
89 0 : onToolsCall(request, onResponse);
90 0 : } else if (method.startsWith(u"notifications/")) {
91 0 : onNotifications(method);
92 0 : onResponse(JsonObject());
93 : } else {
94 0 : LOGE() << "Unknown method: " << method;
95 0 : replyError(request, -32601, "Method not found", onResponse);
96 : }
97 0 : }
98 :
99 0 : void McpServer::onInitialize(const JsonObject& request, const std::function<void(const JsonObject&)>& onResponse)
100 : {
101 0 : JsonObject response;
102 0 : response["jsonrpc"] = "2.0";
103 0 : response["id"] = request.value("id");
104 :
105 0 : JsonObject result;
106 0 : result["protocolVersion"] = PROTOCOL_VERSION;
107 :
108 0 : JsonObject toolCapabilities;
109 0 : toolCapabilities["listChanged"] = false;
110 :
111 0 : JsonObject capabilities;
112 0 : capabilities["tools"] = toolCapabilities;
113 0 : result["capabilities"] = capabilities;
114 :
115 0 : JsonObject serverInfo;
116 0 : serverInfo["name"] = SERVER_NAME;
117 0 : serverInfo["version"] = m_version;
118 0 : result["serverInfo"] = serverInfo;
119 :
120 0 : response["result"] = result;
121 :
122 0 : onResponse(response);
123 0 : }
124 :
125 0 : void McpServer::onToolsListRequest(const ToolsListHandler& onToolsList)
126 : {
127 0 : m_onToolsList = onToolsList;
128 0 : }
129 :
130 0 : void McpServer::onToolsList(const JsonObject& request, const std::function<void(const JsonObject&)>& onResponse)
131 : {
132 0 : if (!m_onToolsList) {
133 0 : replyError(request, -32603, "No tools list handler", onResponse);
134 0 : return;
135 : }
136 :
137 0 : const JsonValue id = request.value("id");
138 0 : m_onToolsList([id, onResponse](const std::vector<Tool>& tools) {
139 0 : JsonObject response;
140 0 : response["jsonrpc"] = "2.0";
141 0 : response["id"] = id;
142 :
143 0 : JsonArray arr;
144 0 : for (const Tool& tool : tools) {
145 0 : JsonObject obj;
146 0 : obj["name"] = tool.name;
147 0 : obj["title"] = tool.title;
148 0 : obj["description"] = tool.description;
149 :
150 : // input
151 : {
152 0 : const InputSchema& schema = tool.inputSchema;
153 0 : JsonObject schemaObj;
154 0 : schemaObj["type"] = schema.type;
155 0 : JsonObject properties;
156 0 : for (const auto& [name, property] : schema.properties) {
157 0 : JsonObject propertyObj;
158 0 : propertyObj["type"] = to_string(property.type.type);
159 0 : if (property.type.format != Format::Undefined) {
160 0 : propertyObj["format"] = to_string(property.type.format);
161 : }
162 0 : propertyObj["description"] = property.description;
163 0 : if (!property.minimum.isNull()) {
164 0 : propertyObj["minimum"] = JsonValue::fromVal(property.minimum);
165 : }
166 0 : if (!property.maximum.isNull()) {
167 0 : propertyObj["maximum"] = JsonValue::fromVal(property.maximum);
168 : }
169 0 : properties[name] = propertyObj;
170 0 : }
171 0 : schemaObj["properties"] = properties;
172 0 : obj["inputSchema"] = schemaObj;
173 0 : }
174 :
175 : // output
176 : {
177 0 : const OutputSchema& schema = tool.outputSchema;
178 0 : JsonObject schemaObj;
179 0 : schemaObj["type"] = schema.type;
180 0 : JsonObject properties;
181 0 : for (const auto& [name, property] : schema.properties) {
182 0 : JsonObject propertyObj;
183 0 : propertyObj["type"] = to_string(property.type.type);
184 0 : if (property.type.format != Format::Undefined) {
185 0 : propertyObj["format"] = to_string(property.type.format);
186 : }
187 0 : propertyObj["description"] = property.description;
188 0 : properties[name] = propertyObj;
189 0 : }
190 0 : schemaObj["properties"] = properties;
191 0 : obj["outputSchema"] = schemaObj;
192 0 : }
193 :
194 0 : arr << obj;
195 0 : }
196 :
197 0 : JsonObject result;
198 0 : result["tools"] = arr;
199 :
200 0 : response["result"] = result;
201 :
202 0 : onResponse(response);
203 0 : });
204 0 : }
205 :
206 0 : void McpServer::onToolsCallRequest(const ToolsCallHandler& onToolsCall)
207 : {
208 0 : m_onToolsCall = onToolsCall;
209 0 : }
210 :
211 0 : void McpServer::onToolsCall(const JsonObject& request, const std::function<void(const JsonObject&)>& onResponse)
212 : {
213 0 : if (!m_onToolsCall) {
214 0 : replyError(request, -32603, "No tools call handler", onResponse);
215 0 : return;
216 : }
217 :
218 0 : const JsonValue id = request.value("id");
219 0 : const JsonObject params = request.value("params").toObject();
220 0 : const std::string name = params.value("name").toString().toStdString();
221 0 : const JsonObject arguments = params.value("arguments").toObject();
222 :
223 0 : m_onToolsCall(name, toArgs(arguments), [id, onResponse](const ToolResult& toolResult) {
224 0 : JsonObject response;
225 0 : response["jsonrpc"] = "2.0";
226 0 : response["id"] = id;
227 :
228 0 : JsonObject resObj;
229 0 : JsonArray content;
230 0 : bool isError = false;
231 :
232 0 : if (toolResult.ret) {
233 0 : JsonObject structuredContent;
234 0 : for (const auto& [name, val] : toolResult.result) {
235 0 : structuredContent[name] = JsonValue::fromVal(val);
236 : }
237 0 : resObj["structuredContent"] = structuredContent;
238 :
239 0 : JsonObject contentObj;
240 0 : contentObj["type"] = "text";
241 0 : contentObj["text"] = JsonDocument(structuredContent).toJson(JsonDocument::Format::Compact).constChar();
242 0 : content.append(contentObj);
243 0 : } else {
244 0 : isError = true;
245 0 : JsonObject errText;
246 0 : errText["type"] = "text";
247 0 : errText["text"] = toolResult.ret.toString();
248 0 : content.append(errText);
249 0 : }
250 :
251 0 : resObj["content"] = content;
252 0 : resObj["isError"] = isError;
253 :
254 0 : response["result"] = resObj;
255 0 : onResponse(response);
256 0 : });
257 0 : }
258 :
259 0 : void McpServer::onNotifications(const String& method)
260 : {
261 0 : LOGI() << "Notification: " << method;
262 0 : }
263 :
264 0 : void McpServer::replyError(const JsonObject& request, int code, const std::string& message,
265 : const std::function<void(const JsonObject&)>& onResponse)
266 : {
267 0 : JsonObject error;
268 0 : error["code"] = code;
269 0 : error["message"] = message;
270 :
271 0 : JsonObject response;
272 0 : response["jsonrpc"] = "2.0";
273 0 : response["id"] = request.value("id");
274 0 : response["error"] = error;
275 :
276 0 : onResponse(response);
277 0 : }
|