10#include <QCoreApplication>
13#include <QJsonDocument>
35# include <QSocketNotifier>
40StdinReader::StdinReader(QObject *parent)
45void StdinReader::requestStop()
47 m_stopRequested =
true;
50void StdinReader::run()
57 while (!m_stopRequested) {
59 bool overflowed =
false;
60 int ch = std::char_traits<char>::eof();
62 while (!m_stopRequested) {
64 if (ch == std::char_traits<char>::eof() || ch ==
'\n') {
68 line.push_back(
static_cast<char>(ch));
74 if (std::cin.eof() && line.empty() && !overflowed) {
80 }
else if (!line.empty()) {
81 emit dataReceived(QString::fromStdString(line));
84 if (ch == std::char_traits<char>::eof()) {
93 , m_mainWindow(mainWindow)
94 , m_validator(std::make_unique<
MCPValidator>(QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(
"schema-mcp.json")))
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()))
106 const auto addRoutes = [&](
BaseHandler *handler,
const QStringList &methods) {
107 for (
const auto &method : methods) {
108 m_dispatchMap.insert(method, handler);
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"
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"
124 addRoutes(m_connectionHandler.get(), {
125 "connect_elements",
"disconnect_elements",
"list_connections",
"split_connection"
127 addRoutes(m_simulationHandler.get(), {
128 "simulation_control",
"create_waveform",
"export_waveform"
130 addRoutes(m_icHandler.get(), {
131 "create_ic",
"instantiate_ic",
"list_ics",
132 "embed_ic",
"extract_ic"
134 addRoutes(m_historyHandler.get(), {
135 "undo",
"redo",
"get_undo_stack"
137 addRoutes(m_themeHandler.get(), {
138 "get_theme",
"set_theme",
"get_effective_theme"
141 if (!m_validator->isSchemaLoaded()) {
142 qWarning() <<
"MCP schema not loaded — all requests will be rejected";
156 m_stdinReader =
new StdinReader(
this);
157 connect(m_stdinReader, &StdinReader::dataReceived,
this, &MCPProcessor::processIncomingData);
159 connect(m_stdinReader, &StdinReader::finished, qApp, &QCoreApplication::quit);
160 m_stdinReader->start();
164 const int fd = ::fileno(stdin);
165 const int flags = ::fcntl(fd, F_GETFL, 0);
167 ::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
169 m_stdinNotifier =
new QSocketNotifier(fd, QSocketNotifier::Read,
this);
170 connect(m_stdinNotifier, &QSocketNotifier::activated,
this, &MCPProcessor::onStdinReadable);
171 m_stdinNotifier->setEnabled(
true);
178 if (m_stdinReader && m_stdinReader->isRunning()) {
179 m_stdinReader->requestStop();
182 CloseHandle(GetStdHandle(STD_INPUT_HANDLE));
183 m_stdinReader->wait();
186 if (m_stdinNotifier) {
187 m_stdinNotifier->setEnabled(
false);
193void MCPProcessor::onStdinReadable()
197 const ssize_t bytesRead = ::read(::fileno(stdin), buffer,
sizeof(buffer));
200 const QByteArray chunk(buffer,
static_cast<qsizetype
>(bytesRead));
202 for (
const QString &line : lines) {
203 processIncomingData(line);
208 if (bytesRead == 0) {
210 m_stdinNotifier->setEnabled(
false);
211 QCoreApplication::quit();
216#if defined(EWOULDBLOCK) && (EWOULDBLOCK != EAGAIN)
217 if (errno == EAGAIN || errno == EWOULDBLOCK) {
219 if (errno == EAGAIN) {
223 if (errno == EINTR) {
227 m_stdinNotifier->setEnabled(
false);
228 QCoreApplication::quit();
240 while ((newline = buffer.indexOf(
'\n')) != -1) {
241 lines.append(QString::fromUtf8(buffer.left(newline)));
242 buffer.remove(0, newline + 1);
248 qWarning() <<
"MCPProcessor: stdin line exceeds" <<
kMaxStdinLineBytes <<
"bytes with no newline — dropping it";
255void MCPProcessor::processIncomingData(
const QString &line)
258 QString trimmedLine = line.trimmed();
259 if (!trimmedLine.isEmpty()) {
260 processCommand(trimmedLine);
264void MCPProcessor::processCommand(
const QString &line)
267 QJsonParseError parseError;
268 QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &parseError);
270 if (parseError.error != QJsonParseError::NoError) {
271 sendResponse(m_serverInfoHandler->createErrorResponse(QString(
"Invalid JSON: %1").arg(parseError.errorString()),
276 QJsonObject request = doc.object();
279 QJsonValue requestId = request.value(
"id");
282 QString jsonrpc = request.value(
"jsonrpc").toString();
283 if (jsonrpc !=
"2.0") {
284 sendResponse(m_serverInfoHandler->createErrorResponse(
"Invalid or missing jsonrpc version",
291 ValidationResult validationResult = m_validator->validateCommand(request);
292 if (!validationResult.
isValid) {
295 if (!validationResult.
errorPath.isEmpty()) {
296 detailedError += QString(
" (at path: %1)").arg(validationResult.
errorPath);
299 sendResponse(m_serverInfoHandler->createErrorResponse(QString(
"Schema validation failed: %1").arg(detailedError),
305 QString method = request.value(
"method").toString();
306 QJsonObject params = request.value(
"params").toObject();
309 QJsonObject response;
312 if (
auto *handler = m_dispatchMap.value(method)) {
313 response = handler->handleCommand(method, params, requestId);
315 response = m_serverInfoHandler->createErrorResponse(QString(
"Unknown method: %1").arg(method),
318 }
catch (
const std::exception &e) {
319 response = m_serverInfoHandler->createErrorResponse(QString(
"Internal error: %1").arg(e.what()),
322 response = m_serverInfoHandler->createErrorResponse(
"Internal error: unknown exception",
328 ValidationResult validationResult = m_validator->validateResponse(response, method);
329 if (!validationResult.
isValid) {
331 if (!validationResult.
errorPath.isEmpty()) {
332 detailedError += QString(
" (at path: %1)").arg(validationResult.
errorPath);
334 QJsonObject errorResponse = m_serverInfoHandler->createErrorResponse(QString(
"Internal validation error: %1").arg(detailedError),
336 sendResponse(errorResponse);
341 sendResponse(response);
344void MCPProcessor::sendResponse(
const QJsonObject &response)
346 QJsonDocument doc(response);
347 QByteArray data = doc.toJson(QJsonDocument::Compact);
348 m_stdout << QString::fromUtf8(data) << Qt::endl;
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.
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.).
Handler for undo/redo commands.
Handler for IC (Integrated Circuit) commands.
MCPProcessor(MainWindow *mainWindow, QObject *parent=nullptr)
static constexpr qint64 kMaxStdinLineBytes
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.
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.).