wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
MCPValidator.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 <QCoreApplication>
7#include <QDebug>
8#include <QDir>
9#include <QFile>
10#include <QJsonArray>
11#include <QJsonDocument>
12#include <QRegularExpression>
13
15 : m_schemaPath(schemaPath)
16{
17 // Load schema but don't fail constructor if it fails
18 loadSchema(schemaPath);
19}
20
22
23bool MCPValidator::loadSchema(const QString &schemaPath)
24{
25 m_schemaPath = schemaPath;
26 m_schemaLoaded = false;
27
28 QFile schemaFile(schemaPath);
29 if (!schemaFile.open(QIODevice::ReadOnly)) {
30 return false;
31 }
32
33 QByteArray schemaData = schemaFile.readAll();
34 schemaFile.close();
35
36 try {
37 // Parse schema JSON
38 m_schema = json::parse(schemaData.toStdString());
39
40 // Create native validator
41 m_validator = std::make_unique<json_validator>();
42 m_validator->set_root_schema(m_schema);
43
44 // Extract command and response schemas for easier access
45 if (m_schema.contains("properties")) {
46 auto properties = m_schema["properties"];
47 if (properties.contains("commands") && properties["commands"].contains("properties")) {
48 m_commandSchemas = properties["commands"]["properties"];
49 }
50 if (properties.contains("responses") && properties["responses"].contains("properties")) {
51 m_responseSchemas = properties["responses"]["properties"];
52 }
53 }
54
55 m_schemaLoaded = true;
56 return true;
57
58 } catch (const json::parse_error &) {
59 return false;
60 } catch (const json::exception &) {
61 return false;
62 } catch (const std::exception &) {
63 return false;
64 }
65}
66
68{
69 return m_schemaLoaded;
70}
71
73{
74 return m_schemaPath;
75}
76
78{
79 try {
80 return validateCommand(qjsonToNlohmann(command));
81 } catch (const std::exception &e) {
82 return ValidationResult(false, QString("Malformed command: %1").arg(e.what()));
83 }
84}
85
87{
88 if (!m_schemaLoaded) {
89 return ValidationResult(false, "Schema not loaded");
90 }
91
92 // Extract method type (JSON-RPC 2.0 format)
93 QString commandType = "unknown";
94 if (command.contains("method") && command["method"].is_string()) {
95 commandType = QString::fromStdString(command["method"].get<std::string>());
96 } else {
97 return ValidationResult(false, "Missing or invalid 'method' field", "", commandType);
98 }
99
100 // Find the appropriate command schema
101 json commandSchema = findCommandSchema(commandType);
102 if (commandSchema.is_null()) {
103 return ValidationResult(false,
104 QString("No schema found for command type: %1").arg(commandType),
105 "", commandType);
106 }
107
108 // Validate against the command schema using native validator
109 return validateAgainstSchema(command, commandSchema, commandType, "command:" + commandType.toStdString());
110}
111
112ValidationResult MCPValidator::validateResponse(const QJsonObject &response, const QString &expectedCommand)
113{
114 try {
115 return validateResponse(qjsonToNlohmann(response), expectedCommand);
116 } catch (const std::exception &e) {
117 return ValidationResult(false, QString("Malformed response: %1").arg(e.what()));
118 }
119}
120
121ValidationResult MCPValidator::validateResponse(const json &response, const QString &expectedCommand)
122{
123 if (!m_schemaLoaded) {
124 return ValidationResult(false, "Schema not loaded");
125 }
126
127 // First validate against base CommandResponse schema
128 json baseResponseSchema;
129 if (m_schema.contains("definitions") && m_schema["definitions"].contains("CommandResponse")) {
130 baseResponseSchema = m_schema["definitions"]["CommandResponse"];
131 } else {
132 return ValidationResult(false, "CommandResponse schema not found in definitions");
133 }
134
135 ValidationResult baseResult = validateAgainstSchema(response, baseResponseSchema, expectedCommand, "response:base");
136 if (!baseResult.isValid) {
137 return ValidationResult(false,
138 QString("Base response validation failed: %1").arg(baseResult.errorMessage),
139 "base_response", expectedCommand);
140 }
141
142 // If we have a specific expected command, validate against specific response schema
143 if (!expectedCommand.isEmpty()) {
144 json responseSchema = findResponseSchema(expectedCommand);
145 if (!responseSchema.is_null()) {
146 ValidationResult specificResult = validateAgainstSchema(response, responseSchema, expectedCommand, "response:" + expectedCommand.toStdString());
147 if (!specificResult.isValid) {
148 return ValidationResult(false,
149 QString("Specific response validation failed: %1").arg(specificResult.errorMessage),
150 expectedCommand + "_response", expectedCommand);
151 }
152 return ValidationResult(true, "", expectedCommand + "_response", expectedCommand);
153 }
154 }
155
156 return ValidationResult(true, "", "base_response", expectedCommand);
157}
158
159ValidationResult MCPValidator::validateAgainstSchema(const json &data, const json &schema, const QString &commandType, const std::string &cacheKey)
160{
161 try {
162 // Use the main validator which has access to all definitions for $ref resolution
163 if (m_validator) {
164 json_validator *validator = nullptr;
165
166 // Schemas are immutable after load — compile each one once (F44);
167 // a fresh json_validator per call was the dominant per-request cost.
168 const auto it = m_validatorCache.find(cacheKey);
169 if (it != m_validatorCache.end()) {
170 validator = it->second.get();
171 } else {
172 // For schema references that may contain $ref, we need to create a complete schema
173 json fullSchema = schema;
174
175 // If this is a sub-schema without definitions but contains $ref,
176 // we need to include the definitions section
177 if (schema.contains("properties") && m_schema.contains("definitions")) {
178 fullSchema["definitions"] = m_schema["definitions"];
179 fullSchema["$schema"] = m_schema["$schema"];
180 }
181
182 auto compiled = std::make_unique<json_validator>();
183 compiled->set_root_schema(fullSchema);
184 validator = m_validatorCache.emplace(cacheKey, std::move(compiled)).first->second.get();
185 }
186
187 // This throws on validation failure with detailed error messages
188 validator->validate(data);
189 }
190
191 return ValidationResult(true, "", "", commandType);
192
193 } catch (const std::exception &e) {
194 // Native error messages are much better than our manual ones
195 QString errorMsg = QString::fromStdString(e.what());
196 QString errorPath = extractErrorPath(errorMsg);
197
198 return ValidationResult(false, errorMsg, "", commandType, errorPath);
199 }
200}
201
202json MCPValidator::findCommandSchema(const QString &commandName) const
203{
204 if (!m_commandSchemas.is_null() && m_commandSchemas.contains(commandName.toStdString())) {
205 json schema = m_commandSchemas[commandName.toStdString()];
206
207 // Handle $ref resolution
208 if (schema.contains("$ref") && schema["$ref"].is_string()) {
209 QString refPath = QString::fromStdString(schema["$ref"].get<std::string>());
210 if (refPath.startsWith("#/definitions/")) {
211 QString defName = refPath.mid(14); // Remove "#/definitions/"
212 if (m_schema.contains("definitions") && m_schema["definitions"].contains(defName.toStdString())) {
213 return m_schema["definitions"][defName.toStdString()];
214 }
215 }
216 }
217
218 return schema;
219 }
220
221 return json();
222}
223
224json MCPValidator::findResponseSchema(const QString &commandName) const
225{
226 if (!m_responseSchemas.is_null() && m_responseSchemas.contains(commandName.toStdString())) {
227 json schema = m_responseSchemas[commandName.toStdString()];
228
229 // Handle $ref resolution
230 if (schema.contains("$ref") && schema["$ref"].is_string()) {
231 QString refPath = QString::fromStdString(schema["$ref"].get<std::string>());
232 if (refPath.startsWith("#/definitions/")) {
233 QString defName = refPath.mid(14); // Remove "#/definitions/"
234 if (m_schema.contains("definitions") && m_schema["definitions"].contains(defName.toStdString())) {
235 return m_schema["definitions"][defName.toStdString()];
236 }
237 }
238 }
239
240 return schema;
241 }
242
243 return json();
244}
245
246QString MCPValidator::extractErrorPath(const QString &errorMsg)
247{
248 // Extract JSON path from error message using regex
249 // Native validator typically includes paths like "at '/path/to/property'"
250 static const QRegularExpression pathRegex(R"(at\s+'([^']+)')");
251 QRegularExpressionMatch match = pathRegex.match(errorMsg);
252 if (match.hasMatch()) {
253 return match.captured(1);
254 }
255
256 return QString();
257}
258
259json MCPValidator::qjsonToNlohmann(const QJsonObject &qjson)
260{
261 QJsonDocument doc(qjson);
262 QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
263 try {
264 return json::parse(jsonData.toStdString());
265 } catch (const json::parse_error &e) {
266 throw std::runtime_error(std::string("qjsonToNlohmann: JSON parse error: ") + e.what());
267 }
268}
269
270QJsonObject MCPValidator::nlohmannToQJson(const json &nlohmannJson)
271{
272 std::string jsonString = nlohmannJson.dump();
273 QJsonParseError parseError;
274 QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(jsonString), &parseError);
275 if (parseError.error != QJsonParseError::NoError || doc.isNull()) {
276 qWarning() << "nlohmannToQJson: failed to parse JSON:" << parseError.errorString();
277 return {};
278 }
279 return doc.object();
280}
nlohmann::json json
static json qjsonToNlohmann(const QJsonObject &qjson)
ValidationResult validateCommand(const QJsonObject &command)
json findResponseSchema(const QString &commandName) const
MCPValidator(const QString &schemaPath)
static QJsonObject nlohmannToQJson(const json &nlohmannJson)
QString schemaPath() const
bool isSchemaLoaded() const
ValidationResult validateResponse(const QJsonObject &response, const QString &expectedCommand=QString())
json findCommandSchema(const QString &commandName) const
Result of JSON schema validation.