wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
MCPProcessor.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <iostream>
7#include <memory>
8#include <string>
9
10#include <QCoreApplication>
11#include <QDebug>
12#include <QDir>
13#include <QJsonDocument>
14
15#include "App/UI/MainWindow.h"
27
28#ifdef Q_OS_WIN
29# include <windows.h>
30#else
31# include <cerrno>
32# include <fcntl.h>
33# include <unistd.h>
34
35# include <QSocketNotifier>
36#endif
37
38#ifdef Q_OS_WIN
39// StdinReader implementation (Windows only — see header).
40StdinReader::StdinReader(QObject *parent)
41 : QThread(parent)
42{
43}
44
45void StdinReader::requestStop()
46{
47 m_stopRequested = true;
48}
49
50void StdinReader::run()
51{
52 // Blocks reading one character at a time; MCPProcessor::stopProcessing closes the
53 // stdin handle to unblock it, so the loop exits on EOF without QThread::terminate().
54 // A manual char loop (rather than std::getline, which has no length bound) lets a
55 // line past MCPProcessor::kMaxStdinLineBytes be dropped instead of growing std::string without limit.
56 std::string line;
57 while (!m_stopRequested) {
58 line.clear();
59 bool overflowed = false;
60 int ch = std::char_traits<char>::eof();
61
62 while (!m_stopRequested) {
63 ch = std::cin.get();
64 if (ch == std::char_traits<char>::eof() || ch == '\n') {
65 break;
66 }
67 if (static_cast<qint64>(line.size()) < MCPProcessor::kMaxStdinLineBytes) {
68 line.push_back(static_cast<char>(ch));
69 } else {
70 overflowed = true; // keep draining this line without growing it further
71 }
72 }
73
74 if (std::cin.eof() && line.empty() && !overflowed) {
75 break; // clean EOF, nothing pending
76 }
77
78 if (overflowed) {
79 qWarning() << "MCPProcessor: stdin line exceeds" << MCPProcessor::kMaxStdinLineBytes << "bytes with no newline — dropping it";
80 } else if (!line.empty()) {
81 emit dataReceived(QString::fromStdString(line));
82 }
83
84 if (ch == std::char_traits<char>::eof()) {
85 break;
86 }
87 }
88}
89#endif
90
91MCPProcessor::MCPProcessor(MainWindow *mainWindow, QObject *parent)
92 : QObject(parent)
93 , m_mainWindow(mainWindow)
94 , m_validator(std::make_unique<MCPValidator>(QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("schema-mcp.json")))
95 , m_stdout(stdout)
96 , m_serverInfoHandler(std::make_unique<ServerInfoHandler>(mainWindow, m_validator.get()))
97 , m_fileHandler(std::make_unique<FileHandler>(mainWindow, m_validator.get()))
98 , m_elementHandler(std::make_unique<ElementHandler>(mainWindow, m_validator.get()))
99 , m_connectionHandler(std::make_unique<ConnectionHandler>(mainWindow, m_validator.get()))
100 , m_simulationHandler(std::make_unique<SimulationHandler>(mainWindow, m_validator.get()))
101 , m_icHandler(std::make_unique<ICHandler>(mainWindow, m_validator.get()))
102 , m_historyHandler(std::make_unique<HistoryHandler>(mainWindow, m_validator.get()))
103 , m_themeHandler(std::make_unique<ThemeHandler>(mainWindow, m_validator.get()))
104{
105 // Build the method → handler dispatch map
106 const auto addRoutes = [&](BaseHandler *handler, const QStringList &methods) {
107 for (const auto &method : methods) {
108 m_dispatchMap.insert(method, handler);
109 }
110 };
111 addRoutes(m_serverInfoHandler.get(), {"get_server_info", "list_commands", "describe_command"});
112 addRoutes(m_fileHandler.get(), {
113 "load_circuit", "save_circuit", "new_circuit", "close_circuit",
114 "get_tab_count", "export_image",
115 "export_arduino", "export_systemverilog"
116 });
117 addRoutes(m_elementHandler.get(), {
118 "create_element", "delete_element", "list_elements", "move_element",
119 "set_element_properties", "set_input_value", "get_output_value",
120 "rotate_element", "flip_element", "update_element",
121 "change_input_size", "change_output_size",
122 "toggle_truth_table_output", "morph_element"
123 });
124 addRoutes(m_connectionHandler.get(), {
125 "connect_elements", "disconnect_elements", "list_connections", "split_connection"
126 });
127 addRoutes(m_simulationHandler.get(), {
128 "simulation_control", "create_waveform", "export_waveform"
129 });
130 addRoutes(m_icHandler.get(), {
131 "create_ic", "instantiate_ic", "list_ics",
132 "embed_ic", "extract_ic"
133 });
134 addRoutes(m_historyHandler.get(), {
135 "undo", "redo", "get_undo_stack"
136 });
137 addRoutes(m_themeHandler.get(), {
138 "get_theme", "set_theme", "get_effective_theme"
139 });
140
141 if (!m_validator->isSchemaLoaded()) {
142 qWarning() << "MCP schema not loaded — all requests will be rejected";
143 }
144}
145
147{
149 // Smart pointers automatically clean up handlers and validator
150}
151
153{
154 // Start processing without sending automatic messages - follow MCP protocol.
155#ifdef Q_OS_WIN
156 m_stdinReader = new StdinReader(this);
157 connect(m_stdinReader, &StdinReader::dataReceived, this, &MCPProcessor::processIncomingData);
158 // EOF (client closed stdin) ends the thread — shut the processor down too.
159 connect(m_stdinReader, &StdinReader::finished, qApp, &QCoreApplication::quit);
160 m_stdinReader->start();
161#else
162 // Event-driven, no thread: watch stdin in the main event loop. The fd is
163 // made non-blocking so onStdinReadable() can drain it without ever parking.
164 const int fd = ::fileno(stdin);
165 const int flags = ::fcntl(fd, F_GETFL, 0);
166 if (flags != -1) {
167 ::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
168 }
169 m_stdinNotifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
170 connect(m_stdinNotifier, &QSocketNotifier::activated, this, &MCPProcessor::onStdinReadable);
171 m_stdinNotifier->setEnabled(true);
172#endif
173}
174
176{
177#ifdef Q_OS_WIN
178 if (m_stdinReader && m_stdinReader->isRunning()) {
179 m_stdinReader->requestStop();
180 // Close the stdin handle to unblock the thread parked in getline so it
181 // returns on EOF — no QThread::terminate() (which leaks locks/state).
182 CloseHandle(GetStdHandle(STD_INPUT_HANDLE));
183 m_stdinReader->wait();
184 }
185#else
186 if (m_stdinNotifier) {
187 m_stdinNotifier->setEnabled(false);
188 }
189#endif
190}
191
192#ifndef Q_OS_WIN
193void MCPProcessor::onStdinReadable()
194{
195 char buffer[4096];
196 for (;;) {
197 const ssize_t bytesRead = ::read(::fileno(stdin), buffer, sizeof(buffer));
198
199 if (bytesRead > 0) {
200 const QByteArray chunk(buffer, static_cast<qsizetype>(bytesRead));
201 const QStringList lines = extractStdinLines(m_stdinBuffer, chunk);
202 for (const QString &line : lines) {
203 processIncomingData(line);
204 }
205 continue; // drain whatever else is buffered before yielding
206 }
207
208 if (bytesRead == 0) {
209 // EOF: the client closed stdin — shut the processor down cleanly.
210 m_stdinNotifier->setEnabled(false);
211 QCoreApplication::quit();
212 return;
213 }
214
215 // bytesRead < 0
216#if defined(EWOULDBLOCK) && (EWOULDBLOCK != EAGAIN)
217 if (errno == EAGAIN || errno == EWOULDBLOCK) {
218#else
219 if (errno == EAGAIN) { // EWOULDBLOCK == EAGAIN on this platform
220#endif
221 return; // no more data right now; wait for the next activation
222 }
223 if (errno == EINTR) {
224 continue; // interrupted by a signal; retry
225 }
226 // Unrecoverable read error: stop watching and quit.
227 m_stdinNotifier->setEnabled(false);
228 QCoreApplication::quit();
229 return;
230 }
231}
232#endif
233
234QStringList MCPProcessor::extractStdinLines(QByteArray &buffer, const QByteArray &data)
235{
236 buffer.append(data);
237
238 QStringList lines;
239 qsizetype newline;
240 while ((newline = buffer.indexOf('\n')) != -1) {
241 lines.append(QString::fromUtf8(buffer.left(newline)));
242 buffer.remove(0, newline + 1);
243 }
244
245 // A client that never sends '\n' would otherwise grow the buffer without bound —
246 // stdin is not a trusted channel in --mcp/--mcp-gui mode.
247 if (buffer.size() > kMaxStdinLineBytes) {
248 qWarning() << "MCPProcessor: stdin line exceeds" << kMaxStdinLineBytes << "bytes with no newline — dropping it";
249 buffer.clear();
250 }
251
252 return lines;
253}
254
255void MCPProcessor::processIncomingData(const QString &line)
256{
257 // Process the line received from the stdin reader thread
258 QString trimmedLine = line.trimmed();
259 if (!trimmedLine.isEmpty()) {
260 processCommand(trimmedLine);
261 }
262}
263
264void MCPProcessor::processCommand(const QString &line)
265{
266 // Parse JSON command
267 QJsonParseError parseError;
268 QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &parseError);
269
270 if (parseError.error != QJsonParseError::NoError) {
271 sendResponse(m_serverInfoHandler->createErrorResponse(QString("Invalid JSON: %1").arg(parseError.errorString()),
272 QJsonValue(), JsonRpcError::ParseError));
273 return;
274 }
275
276 QJsonObject request = doc.object();
277
278 // Extract request ID early for proper JSON-RPC 2.0 error responses
279 QJsonValue requestId = request.value("id");
280
281 // JSON-RPC 2.0 format validation
282 QString jsonrpc = request.value("jsonrpc").toString();
283 if (jsonrpc != "2.0") {
284 sendResponse(m_serverInfoHandler->createErrorResponse("Invalid or missing jsonrpc version",
285 requestId, JsonRpcError::InvalidRequest));
286 return;
287 }
288
289 // Schema validation
290 if (m_validator) {
291 ValidationResult validationResult = m_validator->validateCommand(request);
292 if (!validationResult.isValid) {
293 QString detailedError = validationResult.errorMessage;
294
295 if (!validationResult.errorPath.isEmpty()) {
296 detailedError += QString(" (at path: %1)").arg(validationResult.errorPath);
297 }
298
299 sendResponse(m_serverInfoHandler->createErrorResponse(QString("Schema validation failed: %1").arg(detailedError),
300 requestId, JsonRpcError::InvalidParams));
301 return;
302 }
303 }
304
305 QString method = request.value("method").toString();
306 QJsonObject params = request.value("params").toObject();
307
308 // Route command to appropriate handler using delegation pattern
309 QJsonObject response;
310
311 try {
312 if (auto *handler = m_dispatchMap.value(method)) {
313 response = handler->handleCommand(method, params, requestId);
314 } else {
315 response = m_serverInfoHandler->createErrorResponse(QString("Unknown method: %1").arg(method),
317 }
318 } catch (const std::exception &e) {
319 response = m_serverInfoHandler->createErrorResponse(QString("Internal error: %1").arg(e.what()),
320 requestId, JsonRpcError::InternalError);
321 } catch (...) {
322 response = m_serverInfoHandler->createErrorResponse("Internal error: unknown exception",
323 requestId, JsonRpcError::InternalError);
324 }
325
326 // Validate and send response
327 if (m_validator) {
328 ValidationResult validationResult = m_validator->validateResponse(response, method);
329 if (!validationResult.isValid) {
330 QString detailedError = validationResult.errorMessage;
331 if (!validationResult.errorPath.isEmpty()) {
332 detailedError += QString(" (at path: %1)").arg(validationResult.errorPath);
333 }
334 QJsonObject errorResponse = m_serverInfoHandler->createErrorResponse(QString("Internal validation error: %1").arg(detailedError),
335 requestId, JsonRpcError::InternalError);
336 sendResponse(errorResponse);
337 return;
338 }
339 }
340
341 sendResponse(response);
342}
343
344void MCPProcessor::sendResponse(const QJsonObject &response)
345{
346 QJsonDocument doc(response);
347 QByteArray data = doc.toJson(QJsonDocument::Compact);
348 m_stdout << QString::fromUtf8(data) << Qt::endl;
349 m_stdout.flush();
350}
Numeric error codes returned in MCP responses, per JSON-RPC 2.0.
Main application window providing menus, toolbars, and tab management.
Abstract base class for MCP command handlers.
Definition BaseHandler.h:24
Handler for connection operation commands (connect, disconnect, list, split).
Handler for element operation commands (create, delete, modify, transform, etc.).
Handler for file operation commands (load, save, export, etc.).
Definition FileHandler.h:16
Handler for undo/redo commands.
Handler for IC (Integrated Circuit) commands.
Definition ICHandler.h:17
MCPProcessor(MainWindow *mainWindow, QObject *parent=nullptr)
void stopProcessing()
static constexpr qint64 kMaxStdinLineBytes
void startProcessing()
static QStringList extractStdinLines(QByteArray &buffer, const QByteArray &data)
JSON Schema validator for MCP commands and responses using native json-schema-validator.
The top-level application window hosting the tab bar, menus, element palette, and editor.
Definition MainWindow.h:47
Handler for server information commands.
Handler for simulation operation commands (control, waveform generation/export).
Handler for theme management commands.
constexpr int ParseError
Invalid JSON received by the server.
constexpr int MethodNotFound
The requested method does not exist or is unavailable.
constexpr int InvalidRequest
The JSON sent is not a valid Request object.
constexpr int InternalError
Internal JSON-RPC error (last-resort).
constexpr int InvalidParams
Invalid method parameters (missing required, wrong type, etc.).