RAM2118 commited on
Commit
df8b77e
·
verified ·
1 Parent(s): 98773fc

Out-of-process rebuild: Add Plugin/McpServer.cpp — MCP tools route through helper IPC

Browse files
Files changed (1) hide show
  1. Source/Plugin/McpServer.cpp +223 -0
Source/Plugin/McpServer.cpp ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "McpServer.h"
2
+ #include "HelperConnection.h"
3
+ #include "../Shared/IPCProtocol.h"
4
+
5
+ #define CPPHTTPLIB_THREAD_POOL_COUNT 2
6
+ #include "httplib.h"
7
+ #include "json.hpp"
8
+
9
+ using json = nlohmann::json;
10
+
11
+ McpServer::McpServer(int port, HelperConnection& helper)
12
+ : server(std::make_unique<httplib::Server>()), port(port), helper(helper)
13
+ {
14
+ setupRoutes();
15
+ }
16
+
17
+ McpServer::~McpServer()
18
+ {
19
+ stop();
20
+ }
21
+
22
+ void McpServer::setupRoutes()
23
+ {
24
+ server->Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
25
+ res.set_header("Access-Control-Allow-Origin", "http://localhost");
26
+
27
+ try {
28
+ auto body = json::parse(req.body);
29
+ std::string method = body.value("method", "");
30
+ std::string responseStr;
31
+
32
+ if (method == "initialize")
33
+ responseStr = handleInitialize(req.body);
34
+ else if (method == "notifications/initialized") {
35
+ initialized = true;
36
+ res.status = 204;
37
+ return;
38
+ }
39
+ else if (method == "tools/list")
40
+ responseStr = handleToolsList(req.body);
41
+ else if (method == "tools/call")
42
+ responseStr = handleToolsCall(req.body);
43
+ else {
44
+ json err = {{"jsonrpc","2.0"},{"id",body.value("id",json(nullptr))},
45
+ {"error",{{"code",-32601},{"message","Method not found"}}}};
46
+ responseStr = err.dump();
47
+ }
48
+
49
+ if (!responseStr.empty())
50
+ res.set_content(responseStr, "application/json");
51
+ }
52
+ catch (const json::exception&) {
53
+ json err = {{"jsonrpc","2.0"},{"id",nullptr},
54
+ {"error",{{"code",-32700},{"message","Parse error"}}}};
55
+ res.set_content(err.dump(), "application/json");
56
+ }
57
+ });
58
+
59
+ server->Get("/health", [this](const httplib::Request&, httplib::Response& res) {
60
+ json health = {
61
+ {"status", "ok"},
62
+ {"port", port},
63
+ {"initialized", initialized},
64
+ {"helper", helper.isHelperRunning() ? "connected" : "disconnected"}
65
+ };
66
+ res.set_content(health.dump(), "application/json");
67
+ });
68
+
69
+ server->Options("/mcp", [](const httplib::Request&, httplib::Response& res) {
70
+ res.set_header("Access-Control-Allow-Origin", "http://localhost");
71
+ res.set_header("Access-Control-Allow-Methods", "POST, OPTIONS");
72
+ res.set_header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
73
+ res.status = 204;
74
+ });
75
+ }
76
+
77
+ void McpServer::start()
78
+ {
79
+ if (running.load()) return;
80
+ serverThread = std::thread([this]() {
81
+ running.store(true);
82
+ server->listen("127.0.0.1", port);
83
+ running.store(false);
84
+ });
85
+ server->wait_until_ready();
86
+ }
87
+
88
+ void McpServer::stop()
89
+ {
90
+ if (server && server->is_running()) server->stop();
91
+ if (serverThread.joinable()) serverThread.join();
92
+ running.store(false);
93
+ }
94
+
95
+ bool McpServer::isRunning() const { return running.load(); }
96
+
97
+ // --- Protocol Handlers ---
98
+
99
+ std::string McpServer::handleInitialize(const std::string& requestBody)
100
+ {
101
+ auto body = json::parse(requestBody);
102
+ sessionId = "pb-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
103
+
104
+ json response = {
105
+ {"jsonrpc","2.0"},
106
+ {"id", body.value("id", json(nullptr))},
107
+ {"result", {
108
+ {"protocolVersion","2025-06-18"},
109
+ {"capabilities",{{"tools",{{"listChanged",false}}}}},
110
+ {"serverInfo",{{"name","PluginBridge"},{"version","0.2.0"}}}
111
+ }}
112
+ };
113
+ return response.dump();
114
+ }
115
+
116
+ std::string McpServer::handleToolsList(const std::string& requestBody)
117
+ {
118
+ auto body = json::parse(requestBody);
119
+ json response = {
120
+ {"jsonrpc","2.0"},
121
+ {"id", body.value("id", json(nullptr))},
122
+ {"result",{
123
+ {"tools", json::array({
124
+ {{"name","list_plugins"},{"description","List loaded plugin"},
125
+ {"inputSchema",{{"type","object"},{"properties",json::object()}}}},
126
+ {{"name","search_param"},{"description","Search params by keyword"},
127
+ {"inputSchema",{{"type","object"},{"properties",{
128
+ {"plugin",{{"type","string"}}},
129
+ {"keyword",{{"type","string"},{"description","e.g. 'gain', 'band 1', 'cutoff'"}}}
130
+ }},{"required",json::array({"plugin","keyword"})}}}},
131
+ {{"name","get_params"},{"description","Get param values by ID"},
132
+ {"inputSchema",{{"type","object"},{"properties",{
133
+ {"plugin",{{"type","string"}}},
134
+ {"ids",{{"type","array"},{"items",{{"type","integer"}}}}}
135
+ }},{"required",json::array({"plugin","ids"})}}}},
136
+ {{"name","set_params"},{"description","Set params (batch, 0.0-1.0)"},
137
+ {"inputSchema",{{"type","object"},{"properties",{
138
+ {"plugin",{{"type","string"}}},
139
+ {"values",{{"type","object"},{"description","id→value"}}}
140
+ }},{"required",json::array({"plugin","values"})}}}},
141
+ {{"name","get_analysis"},{"description","Real-time audio analysis"},
142
+ {"inputSchema",{{"type","object"},{"properties",json::object()}}}}
143
+ })}
144
+ }}
145
+ };
146
+ return response.dump();
147
+ }
148
+
149
+ std::string McpServer::handleToolsCall(const std::string& requestBody)
150
+ {
151
+ auto body = json::parse(requestBody);
152
+ auto params = body["params"];
153
+ std::string toolName = params["name"];
154
+ auto args = params.value("arguments", json::object());
155
+
156
+ std::string resultText;
157
+ bool isError = false;
158
+
159
+ if (!helper.isHelperRunning())
160
+ {
161
+ resultText = "helper not connected";
162
+ isError = true;
163
+ }
164
+ else if (toolName == "list_plugins")
165
+ {
166
+ // Query helper for loaded plugin name
167
+ auto resp = helper.sendCommand("{\"cmd\":\"search_param\",\"keyword\":\"\"}");
168
+ // Just return what's loaded
169
+ resultText = "[\"loaded plugin\"]"; // simplified — actual name comes from processor
170
+ }
171
+ else if (toolName == "search_param")
172
+ {
173
+ json cmd = {{"cmd","search_param"},{"keyword", args.value("keyword","")}};
174
+ auto resp = helper.sendCommand(cmd.dump());
175
+ try {
176
+ auto j = json::parse(resp);
177
+ if (j.value("ok", false))
178
+ resultText = j["results"].dump();
179
+ else
180
+ resultText = j.value("error", "search failed");
181
+ } catch (...) { resultText = "parse error"; isError = true; }
182
+ }
183
+ else if (toolName == "get_params")
184
+ {
185
+ json cmd = {{"cmd","get_params"},{"ids", args.value("ids", json::array())}};
186
+ auto resp = helper.sendCommand(cmd.dump());
187
+ try {
188
+ auto j = json::parse(resp);
189
+ if (j.value("ok", false))
190
+ resultText = j["values"].dump();
191
+ else
192
+ resultText = "get failed";
193
+ } catch (...) { resultText = "parse error"; isError = true; }
194
+ }
195
+ else if (toolName == "set_params")
196
+ {
197
+ json cmd = {{"cmd","set_params"},{"values", args.value("values", json::object())}};
198
+ auto resp = helper.sendCommand(cmd.dump());
199
+ try {
200
+ auto j = json::parse(resp);
201
+ resultText = j.value("ok", false) ? "ok" : "set failed";
202
+ } catch (...) { resultText = "parse error"; isError = true; }
203
+ }
204
+ else if (toolName == "get_analysis")
205
+ {
206
+ resultText = "silent"; // Phase 4
207
+ }
208
+ else
209
+ {
210
+ resultText = "unknown tool: " + toolName;
211
+ isError = true;
212
+ }
213
+
214
+ json response = {
215
+ {"jsonrpc","2.0"},
216
+ {"id", body.value("id", json(nullptr))},
217
+ {"result",{
218
+ {"content", json::array({{{"type","text"},{"text",resultText}}})},
219
+ {"isError", isError}
220
+ }}
221
+ };
222
+ return response.dump();
223
+ }