RAM2118 commited on
Commit
90c2369
·
verified ·
1 Parent(s): 30fff00

Sprint 1: Add McpServer.cpp — working HTTP server with MCP JSON-RPC

Browse files
Files changed (1) hide show
  1. Source/McpServer.cpp +260 -0
Source/McpServer.cpp ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "McpServer.h"
2
+
3
+ // httplib must be included in exactly ONE .cpp file
4
+ #define CPPHTTPLIB_THREAD_POOL_COUNT 2
5
+ #include "httplib.h"
6
+ #include "json.hpp"
7
+
8
+ using json = nlohmann::json;
9
+
10
+ McpServer::McpServer(int port)
11
+ : server(std::make_unique<httplib::Server>()), port(port)
12
+ {
13
+ setupRoutes();
14
+ }
15
+
16
+ McpServer::~McpServer()
17
+ {
18
+ stop();
19
+ }
20
+
21
+ void McpServer::setupRoutes()
22
+ {
23
+ // Single MCP endpoint — all JSON-RPC goes through here
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
+ }
35
+ else if (method == "notifications/initialized") {
36
+ // Notification — no response needed
37
+ initialized = true;
38
+ res.status = 204;
39
+ return;
40
+ }
41
+ else if (method == "tools/list") {
42
+ responseStr = handleToolsList();
43
+ }
44
+ else if (method == "tools/call") {
45
+ responseStr = handleToolsCall(req.body);
46
+ }
47
+ else {
48
+ json errResponse = {
49
+ {"jsonrpc", "2.0"},
50
+ {"id", body.value("id", json(nullptr))},
51
+ {"error", {{"code", -32601}, {"message", "Method not found"}}}
52
+ };
53
+ responseStr = errResponse.dump();
54
+ }
55
+
56
+ if (!responseStr.empty()) {
57
+ res.set_content(responseStr, "application/json");
58
+ }
59
+ }
60
+ catch (const json::exception& e) {
61
+ json errResponse = {
62
+ {"jsonrpc", "2.0"},
63
+ {"id", nullptr},
64
+ {"error", {{"code", -32700}, {"message", "Parse error"}}}
65
+ };
66
+ res.set_content(errResponse.dump(), "application/json");
67
+ }
68
+ });
69
+
70
+ // Health check — quick way to verify server is alive
71
+ server->Get("/health", [this](const httplib::Request&, httplib::Response& res) {
72
+ json health = {{"status", "ok"}, {"port", port}, {"initialized", initialized}};
73
+ res.set_content(health.dump(), "application/json");
74
+ });
75
+
76
+ // CORS preflight
77
+ server->Options("/mcp", [](const httplib::Request&, httplib::Response& res) {
78
+ res.set_header("Access-Control-Allow-Origin", "http://localhost");
79
+ res.set_header("Access-Control-Allow-Methods", "POST, OPTIONS");
80
+ res.set_header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
81
+ res.status = 204;
82
+ });
83
+ }
84
+
85
+ void McpServer::start()
86
+ {
87
+ if (running.load()) return;
88
+
89
+ serverThread = std::thread([this]() {
90
+ running.store(true);
91
+ server->listen("127.0.0.1", port);
92
+ running.store(false);
93
+ });
94
+
95
+ // Wait until server is actually ready
96
+ server->wait_until_ready();
97
+ }
98
+
99
+ void McpServer::stop()
100
+ {
101
+ if (server && server->is_running()) {
102
+ server->stop();
103
+ }
104
+ if (serverThread.joinable()) {
105
+ serverThread.join();
106
+ }
107
+ running.store(false);
108
+ }
109
+
110
+ bool McpServer::isRunning() const
111
+ {
112
+ return running.load();
113
+ }
114
+
115
+ void McpServer::serverThreadFunc()
116
+ {
117
+ server->listen("127.0.0.1", port);
118
+ }
119
+
120
+ // --- MCP Protocol Handlers ---
121
+
122
+ std::string McpServer::handleInitialize(const std::string& requestBody)
123
+ {
124
+ auto body = json::parse(requestBody);
125
+
126
+ // Generate session ID
127
+ sessionId = "pb-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
128
+
129
+ json response = {
130
+ {"jsonrpc", "2.0"},
131
+ {"id", body.value("id", json(nullptr))},
132
+ {"result", {
133
+ {"protocolVersion", "2025-06-18"},
134
+ {"capabilities", {
135
+ {"tools", {{"listChanged", false}}}
136
+ }},
137
+ {"serverInfo", {
138
+ {"name", "PluginBridge"},
139
+ {"version", "0.1.0"}
140
+ }}
141
+ }}
142
+ };
143
+
144
+ return response.dump();
145
+ }
146
+
147
+ std::string McpServer::handleToolsList()
148
+ {
149
+ json response = {
150
+ {"jsonrpc", "2.0"},
151
+ {"id", 1},
152
+ {"result", {
153
+ {"tools", json::array({
154
+ {
155
+ {"name", "list_plugins"},
156
+ {"description", "List all loaded plugin instances"},
157
+ {"inputSchema", {{"type", "object"}, {"properties", json::object()}}}
158
+ },
159
+ {
160
+ {"name", "search_param"},
161
+ {"description", "Search plugin parameters by keyword"},
162
+ {"inputSchema", {
163
+ {"type", "object"},
164
+ {"properties", {
165
+ {"plugin", {{"type", "string"}, {"description", "Plugin name"}}},
166
+ {"keyword", {{"type", "string"}, {"description", "Search term"}}}
167
+ }},
168
+ {"required", json::array({"plugin", "keyword"})}
169
+ }}
170
+ },
171
+ {
172
+ {"name", "get_params"},
173
+ {"description", "Get current values for parameter IDs"},
174
+ {"inputSchema", {
175
+ {"type", "object"},
176
+ {"properties", {
177
+ {"plugin", {{"type", "string"}}},
178
+ {"ids", {{"type", "array"}, {"items", {{"type", "integer"}}}}}
179
+ }},
180
+ {"required", json::array({"plugin", "ids"})}
181
+ }}
182
+ },
183
+ {
184
+ {"name", "set_params"},
185
+ {"description", "Set parameter values by ID (batch)"},
186
+ {"inputSchema", {
187
+ {"type", "object"},
188
+ {"properties", {
189
+ {"plugin", {{"type", "string"}}},
190
+ {"values", {{"type", "object"}, {"description", "param_id → value (0.0-1.0)"}}}
191
+ }},
192
+ {"required", json::array({"plugin", "values"})}
193
+ }}
194
+ },
195
+ {
196
+ {"name", "get_analysis"},
197
+ {"description", "Get real-time audio analysis (LUFS, bands, stereo)"},
198
+ {"inputSchema", {{"type", "object"}, {"properties", json::object()}}}
199
+ }
200
+ })}
201
+ }}
202
+ };
203
+
204
+ return response.dump();
205
+ }
206
+
207
+ std::string McpServer::handleToolsCall(const std::string& requestBody)
208
+ {
209
+ auto body = json::parse(requestBody);
210
+ std::string toolName = body["params"]["name"];
211
+
212
+ json result;
213
+
214
+ if (toolName == "list_plugins") {
215
+ // Sprint 1: no plugins loaded yet
216
+ result = {
217
+ {"content", json::array({{{"type", "text"}, {"text", "[]"}}})},
218
+ {"isError", false}
219
+ };
220
+ }
221
+ else if (toolName == "search_param") {
222
+ result = {
223
+ {"content", json::array({{{"type", "text"}, {"text", "no plugin loaded"}}})},
224
+ {"isError", true}
225
+ };
226
+ }
227
+ else if (toolName == "get_params") {
228
+ result = {
229
+ {"content", json::array({{{"type", "text"}, {"text", "no plugin loaded"}}})},
230
+ {"isError", true}
231
+ };
232
+ }
233
+ else if (toolName == "set_params") {
234
+ result = {
235
+ {"content", json::array({{{"type", "text"}, {"text", "no plugin loaded"}}})},
236
+ {"isError", true}
237
+ };
238
+ }
239
+ else if (toolName == "get_analysis") {
240
+ // Sprint 1: stub response
241
+ result = {
242
+ {"content", json::array({{{"type", "text"}, {"text", "silent"}}})},
243
+ {"isError", false}
244
+ };
245
+ }
246
+ else {
247
+ result = {
248
+ {"content", json::array({{{"type", "text"}, {"text", "unknown tool: " + toolName}}})},
249
+ {"isError", true}
250
+ };
251
+ }
252
+
253
+ json response = {
254
+ {"jsonrpc", "2.0"},
255
+ {"id", body.value("id", json(nullptr))},
256
+ {"result", result}
257
+ };
258
+
259
+ return response.dump();
260
+ }