@vostride/agent-qa-mcp 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +105 -0
- package/NOTICE.md +11 -0
- package/dist/index.cjs +1128 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +135 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +1111 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
buildAnalyticsEvent as buildAnalyticsEvent2,
|
|
5
|
+
captureAnalytics as captureAnalytics2,
|
|
6
|
+
resolveAnalyticsStandardProperties as resolveAnalyticsStandardProperties2
|
|
7
|
+
} from "@vostride/agent-qa-core";
|
|
8
|
+
|
|
9
|
+
// src/agent-qa-server.ts
|
|
10
|
+
import { readFile } from "fs/promises";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import {
|
|
14
|
+
AgentQaConfigSchema as AgentQaConfigSchema2,
|
|
15
|
+
buildAnalyticsEvent,
|
|
16
|
+
captureAnalytics,
|
|
17
|
+
createAnalyticsService,
|
|
18
|
+
getAgentQaVersion,
|
|
19
|
+
resolveAnalyticsStandardProperties
|
|
20
|
+
} from "@vostride/agent-qa-core";
|
|
21
|
+
import {
|
|
22
|
+
ENTITY_ID_TYPES,
|
|
23
|
+
generateCanonicalId,
|
|
24
|
+
getEntityIdContract,
|
|
25
|
+
getEntityIdContracts as getEntityIdContracts2,
|
|
26
|
+
isCanonicalId
|
|
27
|
+
} from "@vostride/agent-qa-ids";
|
|
28
|
+
import { parse as parseYaml } from "yaml";
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
|
|
31
|
+
// src/schema-reference.ts
|
|
32
|
+
import {
|
|
33
|
+
AgentQaConfigSchema,
|
|
34
|
+
HooksFileSchema,
|
|
35
|
+
SuiteDefinitionSchema,
|
|
36
|
+
TestDefinitionSchema
|
|
37
|
+
} from "@vostride/agent-qa-core";
|
|
38
|
+
import {
|
|
39
|
+
getEntityIdContracts
|
|
40
|
+
} from "@vostride/agent-qa-ids";
|
|
41
|
+
var SCHEMA_REFERENCE_NAMES = ["config", "test", "suite", "hooks", "ids"];
|
|
42
|
+
var VALIDATION_KINDS = ["config", "test", "suite", "hooks"];
|
|
43
|
+
var AGENT_QA_SCHEMA_REFERENCES = {
|
|
44
|
+
config: {
|
|
45
|
+
name: "agent-qa config",
|
|
46
|
+
rootKeys: ["workspace", "services", "registry", "use", "analytics"],
|
|
47
|
+
services: {
|
|
48
|
+
dashboard: ["port", "dbPath", "artifactsDir"],
|
|
49
|
+
mcp: ["enabled", "transport", "host", "port", "path"],
|
|
50
|
+
cache: ["dir", "ttl"],
|
|
51
|
+
logging: ["level"],
|
|
52
|
+
recording: ["enabled"],
|
|
53
|
+
accessibility: ["enabled", "standard", "runAfter", "failOnViolation", "disableRules", "exclude"],
|
|
54
|
+
memory: ["enabled", "provider", "dir", "minTrust", "maxInjections", "curatorEnabled"]
|
|
55
|
+
},
|
|
56
|
+
mcpLocalOnly: {
|
|
57
|
+
transports: ["http", "stdio"],
|
|
58
|
+
hosts: ["localhost", "127.0.0.1", "::1"],
|
|
59
|
+
defaultPort: 3471,
|
|
60
|
+
defaultPath: "/mcp"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
test: {
|
|
64
|
+
name: "agent-qa test YAML",
|
|
65
|
+
requiredKeys: ["test-id", "name", "target", "steps"],
|
|
66
|
+
optionalKeys: ["context", "use", "meta", "setup", "teardown"],
|
|
67
|
+
idType: "test",
|
|
68
|
+
hookReferences: "setup and teardown arrays must contain canonical h_ hook IDs.",
|
|
69
|
+
stepFormats: ["string", "object with step/timeout/retries/screenshot/capture/maxAttempts"]
|
|
70
|
+
},
|
|
71
|
+
suite: {
|
|
72
|
+
name: "agent-qa suite YAML",
|
|
73
|
+
requiredKeys: ["name", "target", "tests"],
|
|
74
|
+
optionalKeys: ["suite-id", "context", "setup", "teardown", "use"],
|
|
75
|
+
idType: "suite",
|
|
76
|
+
testReference: "Each tests[] entry uses { test: path, id: canonical t_ test ID }.",
|
|
77
|
+
hookReferences: "setup and teardown arrays must contain canonical h_ hook IDs."
|
|
78
|
+
},
|
|
79
|
+
hooks: {
|
|
80
|
+
name: "agent-qa hooks file",
|
|
81
|
+
rootKeys: ["hooks"],
|
|
82
|
+
hookKeys: ["id", "name", "runtime", "file", "deps", "packageFile", "timeout", "network"],
|
|
83
|
+
idType: "hook",
|
|
84
|
+
runtimes: ["node", "bun", "python", "bash"]
|
|
85
|
+
},
|
|
86
|
+
ids: {
|
|
87
|
+
name: "agent-qa canonical IDs",
|
|
88
|
+
instruction: "Generate IDs with agent_qa_generate_id or `agent-qa ids generate <type>`; never hand-write IDs.",
|
|
89
|
+
contracts: getEntityIdContracts()
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var VALIDATION_SCHEMAS = {
|
|
93
|
+
config: AgentQaConfigSchema,
|
|
94
|
+
test: TestDefinitionSchema,
|
|
95
|
+
suite: SuiteDefinitionSchema,
|
|
96
|
+
hooks: HooksFileSchema
|
|
97
|
+
};
|
|
98
|
+
function validateAgentQaDefinition(kind, definition) {
|
|
99
|
+
const result = VALIDATION_SCHEMAS[kind].safeParse(definition);
|
|
100
|
+
if (result.success) {
|
|
101
|
+
return {
|
|
102
|
+
valid: true,
|
|
103
|
+
kind,
|
|
104
|
+
issues: []
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
valid: false,
|
|
109
|
+
kind,
|
|
110
|
+
issues: result.error.issues.map((issue) => ({
|
|
111
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
112
|
+
message: issue.message
|
|
113
|
+
}))
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/agent-qa-server.ts
|
|
118
|
+
var DISCOVERY = {
|
|
119
|
+
name: "agent-qa",
|
|
120
|
+
purpose: "Author, validate, run, and triage agent-qa tests through local trusted MCP.",
|
|
121
|
+
tools: [
|
|
122
|
+
"agent_qa_discover",
|
|
123
|
+
"agent_qa_get_config",
|
|
124
|
+
"agent_qa_schema_reference",
|
|
125
|
+
"agent_qa_validate_definition",
|
|
126
|
+
"agent_qa_generate_id",
|
|
127
|
+
"agent_qa_validate_id",
|
|
128
|
+
"agent_qa_list_tests",
|
|
129
|
+
"agent_qa_read_test",
|
|
130
|
+
"agent_qa_validate_test",
|
|
131
|
+
"agent_qa_create_test",
|
|
132
|
+
"agent_qa_update_test",
|
|
133
|
+
"agent_qa_delete_test",
|
|
134
|
+
"agent_qa_list_suites",
|
|
135
|
+
"agent_qa_read_suite",
|
|
136
|
+
"agent_qa_validate_suite",
|
|
137
|
+
"agent_qa_create_suite",
|
|
138
|
+
"agent_qa_update_suite",
|
|
139
|
+
"agent_qa_delete_suite",
|
|
140
|
+
"agent_qa_list_hooks",
|
|
141
|
+
"agent_qa_read_hook",
|
|
142
|
+
"agent_qa_create_hook",
|
|
143
|
+
"agent_qa_update_hook",
|
|
144
|
+
"agent_qa_delete_hook",
|
|
145
|
+
"agent_qa_run_hook",
|
|
146
|
+
"agent_qa_enqueue_test_run",
|
|
147
|
+
"agent_qa_enqueue_suite_run",
|
|
148
|
+
"agent_qa_get_run",
|
|
149
|
+
"agent_qa_get_run_steps",
|
|
150
|
+
"agent_qa_get_run_logs",
|
|
151
|
+
"agent_qa_get_run_execution_logs",
|
|
152
|
+
"agent_qa_get_run_artifact",
|
|
153
|
+
"agent_qa_cancel_run",
|
|
154
|
+
"agent_qa_classify_failure"
|
|
155
|
+
],
|
|
156
|
+
resources: SCHEMA_REFERENCE_NAMES.map((name) => `agent-qa://schema/${name}`),
|
|
157
|
+
prompts: ["agent_qa_authoring_context"],
|
|
158
|
+
phases: {
|
|
159
|
+
current: "discovery-schema-id-contracts",
|
|
160
|
+
next: ["authoring tools", "run execution and triage tools"]
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
function jsonContent(value) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: JSON.stringify(value, null, 2)
|
|
168
|
+
}]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function errorContent(message) {
|
|
172
|
+
return {
|
|
173
|
+
isError: true,
|
|
174
|
+
content: [{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: message
|
|
177
|
+
}]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function categorizeMcpTool(name) {
|
|
181
|
+
if (name === "agent_qa_discover") return "discovery";
|
|
182
|
+
if (name.includes("schema") || name.includes("definition") || name.includes("config")) return "schema";
|
|
183
|
+
if (name.includes("_id")) return "id";
|
|
184
|
+
if (name.includes("_hook")) return "hook";
|
|
185
|
+
if (name.includes("classify")) return "triage";
|
|
186
|
+
if (name.includes("_run") || name.includes("_logs") || name.includes("_artifact") || name.includes("_fix") || name.includes("_failure")) return "run";
|
|
187
|
+
if (name.includes("_test") || name.includes("_suite")) return "authoring";
|
|
188
|
+
return "unknown";
|
|
189
|
+
}
|
|
190
|
+
function isErrorResult(value) {
|
|
191
|
+
return Boolean(value && typeof value === "object" && value.isError === true);
|
|
192
|
+
}
|
|
193
|
+
function isDashboardErrorResult(value) {
|
|
194
|
+
const parsed = parseToolJsonContent(value);
|
|
195
|
+
return Boolean(parsed && typeof parsed === "object" && parsed.ok === false);
|
|
196
|
+
}
|
|
197
|
+
function parseToolJsonContent(value) {
|
|
198
|
+
if (!value || typeof value !== "object") return void 0;
|
|
199
|
+
const content = value.content;
|
|
200
|
+
if (!Array.isArray(content)) return void 0;
|
|
201
|
+
const firstText = content.find(
|
|
202
|
+
(item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string"
|
|
203
|
+
);
|
|
204
|
+
if (!firstText) return void 0;
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(firstText.text);
|
|
207
|
+
} catch {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function categorizeMcpError(errOrResult) {
|
|
212
|
+
if (errOrResult instanceof z.ZodError) return "validation";
|
|
213
|
+
const parsed = parseToolJsonContent(errOrResult);
|
|
214
|
+
const candidate = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
215
|
+
if (candidate && candidate.ok === false) return "dashboard_error";
|
|
216
|
+
if (errOrResult instanceof Error) return "tool_error";
|
|
217
|
+
if (isErrorResult(errOrResult)) return "tool_error";
|
|
218
|
+
return "unknown";
|
|
219
|
+
}
|
|
220
|
+
function extractExplicitCanonicalRunId(input) {
|
|
221
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
|
|
222
|
+
const runId = input.runId;
|
|
223
|
+
return typeof runId === "string" && isCanonicalId("run", runId) ? runId : void 0;
|
|
224
|
+
}
|
|
225
|
+
function extractSuccessfulEnqueueRunId(result) {
|
|
226
|
+
if (isErrorResult(result)) return void 0;
|
|
227
|
+
const parsed = parseToolJsonContent(result);
|
|
228
|
+
const container = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
229
|
+
if (!container) return void 0;
|
|
230
|
+
const body = container.body && typeof container.body === "object" ? container.body : container;
|
|
231
|
+
for (const key of ["runId", "id", "parentRunId"]) {
|
|
232
|
+
const value = body[key];
|
|
233
|
+
if (typeof value === "string" && isCanonicalId("run", value)) return value;
|
|
234
|
+
}
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
function resolveDashboardApiUrl(dashboardUrl, apiPath) {
|
|
238
|
+
const base = dashboardUrl.endsWith("/") ? dashboardUrl : `${dashboardUrl}/`;
|
|
239
|
+
const normalizedPath = apiPath.startsWith("/") ? apiPath.slice(1) : apiPath;
|
|
240
|
+
return new URL(normalizedPath, base).toString();
|
|
241
|
+
}
|
|
242
|
+
function withQuery(path, query) {
|
|
243
|
+
const params = new URLSearchParams();
|
|
244
|
+
for (const [key, value] of Object.entries(query)) {
|
|
245
|
+
if (value !== void 0) params.set(key, String(value));
|
|
246
|
+
}
|
|
247
|
+
const queryString = params.toString();
|
|
248
|
+
return queryString ? `${path}?${queryString}` : path;
|
|
249
|
+
}
|
|
250
|
+
async function callDashboardApi(input) {
|
|
251
|
+
const response = await fetch(resolveDashboardApiUrl(input.dashboardUrl, input.path), {
|
|
252
|
+
method: input.method ?? "GET",
|
|
253
|
+
headers: input.body === void 0 ? void 0 : { "content-type": "application/json" },
|
|
254
|
+
body: input.body === void 0 ? void 0 : JSON.stringify(input.body)
|
|
255
|
+
});
|
|
256
|
+
const text = await response.text();
|
|
257
|
+
let body = null;
|
|
258
|
+
if (text.length > 0) {
|
|
259
|
+
try {
|
|
260
|
+
body = JSON.parse(text);
|
|
261
|
+
} catch {
|
|
262
|
+
body = text;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
ok: response.ok,
|
|
267
|
+
status: response.status,
|
|
268
|
+
body
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function collectStrings(value, output = [], limit = 80) {
|
|
272
|
+
if (output.length >= limit) return output;
|
|
273
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
274
|
+
output.push(value);
|
|
275
|
+
} else if (Array.isArray(value)) {
|
|
276
|
+
for (const item of value) collectStrings(item, output, limit);
|
|
277
|
+
} else if (value && typeof value === "object") {
|
|
278
|
+
for (const child of Object.values(value)) collectStrings(child, output, limit);
|
|
279
|
+
}
|
|
280
|
+
return output;
|
|
281
|
+
}
|
|
282
|
+
function classifyByNeedle(haystack, evidence, needles, category, confidence) {
|
|
283
|
+
const match = needles.find((needle) => haystack.includes(needle));
|
|
284
|
+
if (!match) return null;
|
|
285
|
+
const matchingEvidence = evidence.find((item) => item.toLowerCase().includes(match)) ?? evidence[0] ?? match;
|
|
286
|
+
return {
|
|
287
|
+
category,
|
|
288
|
+
confidence,
|
|
289
|
+
evidence: [matchingEvidence].filter(Boolean).slice(0, 5),
|
|
290
|
+
recentRelatedCount: 0
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function classifyRunFailureFromDashboardData(input) {
|
|
294
|
+
const run = input.runDetail && typeof input.runDetail === "object" ? input.runDetail.run : void 0;
|
|
295
|
+
const status = run && typeof run === "object" ? run.status : void 0;
|
|
296
|
+
const evidence = collectStrings([input.runDetail, input.artifact, input.logs, input.executionLogs]);
|
|
297
|
+
const haystack = evidence.join("\n").toLowerCase();
|
|
298
|
+
const recentRelatedCount = Array.isArray(input.recentRuns?.runs) ? input.recentRuns.runs.length : 0;
|
|
299
|
+
if (status && status !== "failed") {
|
|
300
|
+
return {
|
|
301
|
+
category: "passed",
|
|
302
|
+
confidence: 0.95,
|
|
303
|
+
evidence: [`Run status is ${String(status)}.`],
|
|
304
|
+
recentRelatedCount
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const checks = [
|
|
308
|
+
[["timed out", "timeout"], "timeout", 0.9],
|
|
309
|
+
[["appium server failed", "appium"], "appium_startup", 0.9],
|
|
310
|
+
[["browser closed", "target page, context or browser has been closed", "browser disconnect"], "browser_disconnect", 0.85],
|
|
311
|
+
[["hook_not_runnable", "hook failed", "hook registry", "hook"], "hook_failure", 0.8],
|
|
312
|
+
[["not found", "no element", "strict mode violation", "selector"], "element_not_found", 0.8],
|
|
313
|
+
[["assert", "expected", "verify"], "assertion_failure", 0.75],
|
|
314
|
+
[["econnrefused", "enotfound", "network", "docker"], "infrastructure", 0.7]
|
|
315
|
+
];
|
|
316
|
+
for (const [needles, category, confidence] of checks) {
|
|
317
|
+
const result = classifyByNeedle(haystack, evidence, needles, category, confidence);
|
|
318
|
+
if (result) return { ...result, recentRelatedCount };
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
category: "unknown_failure",
|
|
322
|
+
confidence: 0.3,
|
|
323
|
+
evidence: evidence.slice(0, 5),
|
|
324
|
+
recentRelatedCount
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function maskSensitive(value, parentKey = "") {
|
|
328
|
+
const lower = parentKey.toLowerCase();
|
|
329
|
+
if (typeof value === "string" && (lower.includes("key") || lower.includes("token") || lower.includes("secret") || lower.includes("password"))) {
|
|
330
|
+
return value.length > 8 ? `${value.slice(0, 3)}****${value.slice(-4)}` : "****";
|
|
331
|
+
}
|
|
332
|
+
if (Array.isArray(value)) return value.map((item) => maskSensitive(item, parentKey));
|
|
333
|
+
if (value && typeof value === "object") {
|
|
334
|
+
return Object.fromEntries(
|
|
335
|
+
Object.entries(value).map(([key, child]) => [key, maskSensitive(child, key)])
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
async function readConfig(configPath) {
|
|
341
|
+
const path = resolve(configPath ?? "agent-qa.config.yaml");
|
|
342
|
+
const content = await readFile(path, "utf-8");
|
|
343
|
+
const raw = parseYaml(content) ?? {};
|
|
344
|
+
const validation = AgentQaConfigSchema2.safeParse(raw);
|
|
345
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
346
|
+
const registry = record.registry && typeof record.registry === "object" && !Array.isArray(record.registry) ? record.registry : {};
|
|
347
|
+
const services = record.services && typeof record.services === "object" && !Array.isArray(record.services) ? record.services : {};
|
|
348
|
+
return {
|
|
349
|
+
path,
|
|
350
|
+
raw: maskSensitive(raw),
|
|
351
|
+
valid: validation.success,
|
|
352
|
+
issues: validation.success ? [] : validation.error.issues.map((issue) => ({
|
|
353
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
354
|
+
message: issue.message
|
|
355
|
+
})),
|
|
356
|
+
summary: {
|
|
357
|
+
workspaceConfigured: Boolean(record.workspace),
|
|
358
|
+
serviceKeys: Object.keys(services),
|
|
359
|
+
targetNames: Object.keys(registry.targets ?? {}),
|
|
360
|
+
deviceNames: Object.keys(registry.devices ?? {}),
|
|
361
|
+
providerNames: Object.keys(registry.providers ?? {}),
|
|
362
|
+
llmNames: Array.isArray(registry.llms) ? registry.llms.map((llm) => llm && typeof llm === "object" ? llm.name : void 0).filter((name) => typeof name === "string") : []
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function createAgentQaMcpServer(options = {}) {
|
|
367
|
+
const server = new McpServer({
|
|
368
|
+
name: "agent-qa",
|
|
369
|
+
version: getAgentQaVersion()
|
|
370
|
+
});
|
|
371
|
+
const analyticsService = options.analyticsService ?? createAnalyticsService({ config: options.analyticsConfig });
|
|
372
|
+
const analyticsTransport = options.transport ?? "stdio";
|
|
373
|
+
const registerAnalyticsTool = (name, definition, handler) => server.registerTool(name, definition, (async (input, extra) => {
|
|
374
|
+
const startedAt = Date.now();
|
|
375
|
+
let result;
|
|
376
|
+
let thrown;
|
|
377
|
+
try {
|
|
378
|
+
result = await handler(input, extra);
|
|
379
|
+
return result;
|
|
380
|
+
} catch (err) {
|
|
381
|
+
thrown = err;
|
|
382
|
+
throw err;
|
|
383
|
+
} finally {
|
|
384
|
+
const isError = thrown !== void 0 || isErrorResult(result) || isDashboardErrorResult(result);
|
|
385
|
+
const explicitRunId = extractExplicitCanonicalRunId(input);
|
|
386
|
+
const enqueueRunId = !isError && (name === "agent_qa_enqueue_test_run" || name === "agent_qa_enqueue_suite_run") ? extractSuccessfulEnqueueRunId(result) : void 0;
|
|
387
|
+
try {
|
|
388
|
+
if (options.analyticsConfig?.analytics?.privacy !== true) {
|
|
389
|
+
const standardProperties = options.analyticsStandardProperties ?? await resolveAnalyticsStandardProperties({ surface: "mcp" });
|
|
390
|
+
const event = buildAnalyticsEvent({
|
|
391
|
+
name: "agent-qa.mcp.tool.invoked",
|
|
392
|
+
properties: {
|
|
393
|
+
...standardProperties,
|
|
394
|
+
surface: standardProperties.surface ?? "mcp",
|
|
395
|
+
tool_name: name,
|
|
396
|
+
mcp_tool_category: categorizeMcpTool(name),
|
|
397
|
+
mcp_tool_status: isError ? "error" : "success",
|
|
398
|
+
duration_ms: Math.max(0, Date.now() - startedAt),
|
|
399
|
+
mcp_transport: analyticsTransport,
|
|
400
|
+
...isError ? { mcp_error_category: categorizeMcpError(thrown ?? result) } : {},
|
|
401
|
+
...explicitRunId ?? enqueueRunId ? { run_id: explicitRunId ?? enqueueRunId } : {}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
if (options.analyticsService) {
|
|
405
|
+
await analyticsService.capture(event).catch(() => {
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
await captureAnalytics(event, {
|
|
409
|
+
config: options.analyticsConfig,
|
|
410
|
+
surface: "mcp"
|
|
411
|
+
}).catch(() => {
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}));
|
|
419
|
+
const dashboardUrlField = z.string().url().optional().describe("Dashboard base URL; defaults to the MCP server dashboard URL.");
|
|
420
|
+
const dashboardUrlFor = (inputUrl) => {
|
|
421
|
+
const dashboardUrl = inputUrl ?? options.dashboardUrl;
|
|
422
|
+
if (!dashboardUrl) {
|
|
423
|
+
throw new Error("dashboardUrl is required for authoring tools. Start MCP via `agent-qa dashboard` or pass dashboardUrl.");
|
|
424
|
+
}
|
|
425
|
+
return dashboardUrl;
|
|
426
|
+
};
|
|
427
|
+
const dashboardTool = async (dashboardUrl, path, method, body) => {
|
|
428
|
+
try {
|
|
429
|
+
const result = await callDashboardApi({
|
|
430
|
+
dashboardUrl: dashboardUrlFor(dashboardUrl),
|
|
431
|
+
path,
|
|
432
|
+
method,
|
|
433
|
+
body
|
|
434
|
+
});
|
|
435
|
+
return jsonContent(result);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
return errorContent(err instanceof Error ? err.message : String(err));
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
registerAnalyticsTool(
|
|
441
|
+
"agent_qa_discover",
|
|
442
|
+
{
|
|
443
|
+
title: "Discover agent-qa MCP capabilities",
|
|
444
|
+
description: "Return structured metadata for agent-qa MCP tools, resources, prompts, and current rollout scope.",
|
|
445
|
+
inputSchema: {}
|
|
446
|
+
},
|
|
447
|
+
async () => jsonContent({
|
|
448
|
+
...DISCOVERY,
|
|
449
|
+
endpoint: options.endpointUrl,
|
|
450
|
+
dashboard: options.dashboardUrl,
|
|
451
|
+
configPath: options.configPath
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
registerAnalyticsTool(
|
|
455
|
+
"agent_qa_get_config",
|
|
456
|
+
{
|
|
457
|
+
title: "Inspect agent-qa config",
|
|
458
|
+
description: "Read the active agent-qa config and return a masked raw config plus targets/devices/providers summary.",
|
|
459
|
+
inputSchema: {
|
|
460
|
+
configPath: z.string().optional().describe("Config path; defaults to the MCP server config path or agent-qa.config.yaml"),
|
|
461
|
+
section: z.enum(["raw", "summary"]).optional().describe("Return full masked raw config or summary only")
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
async ({ configPath, section }) => {
|
|
465
|
+
const config = await readConfig(configPath ?? options.configPath);
|
|
466
|
+
return jsonContent(section === "summary" ? { path: config.path, valid: config.valid, issues: config.issues, summary: config.summary } : config);
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
registerAnalyticsTool(
|
|
470
|
+
"agent_qa_schema_reference",
|
|
471
|
+
{
|
|
472
|
+
title: "Get agent-qa schema reference",
|
|
473
|
+
description: "Return structured references for config, test YAML, suite YAML, hooks, or canonical IDs.",
|
|
474
|
+
inputSchema: {
|
|
475
|
+
schema: z.enum(SCHEMA_REFERENCE_NAMES).describe("Reference name to return")
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
async ({ schema }) => jsonContent(AGENT_QA_SCHEMA_REFERENCES[schema])
|
|
479
|
+
);
|
|
480
|
+
registerAnalyticsTool(
|
|
481
|
+
"agent_qa_validate_definition",
|
|
482
|
+
{
|
|
483
|
+
title: "Validate agent-qa definition",
|
|
484
|
+
description: "Validate config/test/suite/hooks objects with the same core schemas agent-qa uses.",
|
|
485
|
+
inputSchema: {
|
|
486
|
+
kind: z.enum(VALIDATION_KINDS).describe("Definition kind"),
|
|
487
|
+
definition: z.unknown().describe("Parsed config/test/suite/hooks object")
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
async ({ kind, definition }) => jsonContent(validateAgentQaDefinition(kind, definition))
|
|
491
|
+
);
|
|
492
|
+
registerAnalyticsTool(
|
|
493
|
+
"agent_qa_generate_id",
|
|
494
|
+
{
|
|
495
|
+
title: "Generate canonical agent-qa ID",
|
|
496
|
+
description: "Generate a canonical id-agent backed ID for a test, suite, hook, run, or observation.",
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: z.enum(ENTITY_ID_TYPES).describe("ID entity type")
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
async ({ type }) => {
|
|
502
|
+
const idType = type;
|
|
503
|
+
return jsonContent({
|
|
504
|
+
id: generateCanonicalId(idType),
|
|
505
|
+
type: idType,
|
|
506
|
+
contract: getEntityIdContract(idType)
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
registerAnalyticsTool(
|
|
511
|
+
"agent_qa_validate_id",
|
|
512
|
+
{
|
|
513
|
+
title: "Validate canonical agent-qa ID",
|
|
514
|
+
description: "Validate canonical id-agent backed IDs and return the expected contract.",
|
|
515
|
+
inputSchema: {
|
|
516
|
+
type: z.enum(ENTITY_ID_TYPES).describe("ID entity type"),
|
|
517
|
+
id: z.string().describe("ID to validate")
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
async ({ type, id }) => {
|
|
521
|
+
const idType = type;
|
|
522
|
+
const contract = getEntityIdContract(idType);
|
|
523
|
+
const valid = isCanonicalId(idType, id);
|
|
524
|
+
return jsonContent({
|
|
525
|
+
valid,
|
|
526
|
+
id,
|
|
527
|
+
type: idType,
|
|
528
|
+
contract,
|
|
529
|
+
message: valid ? "ID is canonical." : `Expected ${contract.prefixWithSeparator} followed by ${contract.words} id-agent words.`
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
registerAnalyticsTool(
|
|
534
|
+
"agent_qa_list_tests",
|
|
535
|
+
{
|
|
536
|
+
title: "List agent-qa tests",
|
|
537
|
+
description: "List tests through the dashboard API using dashboard workspace discovery.",
|
|
538
|
+
inputSchema: { dashboardUrl: dashboardUrlField }
|
|
539
|
+
},
|
|
540
|
+
async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/tests")
|
|
541
|
+
);
|
|
542
|
+
registerAnalyticsTool(
|
|
543
|
+
"agent_qa_read_test",
|
|
544
|
+
{
|
|
545
|
+
title: "Read agent-qa test",
|
|
546
|
+
description: "Read a test by canonical test ID through the dashboard API.",
|
|
547
|
+
inputSchema: {
|
|
548
|
+
dashboardUrl: dashboardUrlField,
|
|
549
|
+
testId: z.string().describe("Canonical t_ test ID")
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
async ({ dashboardUrl, testId }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`)
|
|
553
|
+
);
|
|
554
|
+
registerAnalyticsTool(
|
|
555
|
+
"agent_qa_validate_test",
|
|
556
|
+
{
|
|
557
|
+
title: "Validate agent-qa test YAML",
|
|
558
|
+
description: "Validate test YAML through the dashboard test-file manager.",
|
|
559
|
+
inputSchema: {
|
|
560
|
+
dashboardUrl: dashboardUrlField,
|
|
561
|
+
content: z.string().describe("Test YAML content"),
|
|
562
|
+
filePath: z.string().optional().describe("Optional file path for suite/test routing hints")
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
async ({ dashboardUrl, content, filePath }) => dashboardTool(dashboardUrl, "/api/tests/validate", "POST", { content, filePath })
|
|
566
|
+
);
|
|
567
|
+
registerAnalyticsTool(
|
|
568
|
+
"agent_qa_create_test",
|
|
569
|
+
{
|
|
570
|
+
title: "Create agent-qa test",
|
|
571
|
+
description: "Create a test file through the dashboard workspace-safe file manager.",
|
|
572
|
+
inputSchema: {
|
|
573
|
+
dashboardUrl: dashboardUrlField,
|
|
574
|
+
path: z.string().describe("Workspace-relative test path matched by workspace.testMatch"),
|
|
575
|
+
content: z.string().describe("Test YAML content")
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
async ({ dashboardUrl, path, content }) => dashboardTool(dashboardUrl, "/api/tests", "POST", { path, content })
|
|
579
|
+
);
|
|
580
|
+
registerAnalyticsTool(
|
|
581
|
+
"agent_qa_update_test",
|
|
582
|
+
{
|
|
583
|
+
title: "Update agent-qa test",
|
|
584
|
+
description: "Update a test by canonical test ID through the dashboard API.",
|
|
585
|
+
inputSchema: {
|
|
586
|
+
dashboardUrl: dashboardUrlField,
|
|
587
|
+
testId: z.string().describe("Canonical t_ test ID"),
|
|
588
|
+
content: z.string().describe("Replacement test YAML content")
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
async ({ dashboardUrl, testId, content }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`, "PUT", { content })
|
|
592
|
+
);
|
|
593
|
+
registerAnalyticsTool(
|
|
594
|
+
"agent_qa_delete_test",
|
|
595
|
+
{
|
|
596
|
+
title: "Delete agent-qa test",
|
|
597
|
+
description: "Delete a test by canonical test ID through the dashboard API.",
|
|
598
|
+
inputSchema: {
|
|
599
|
+
dashboardUrl: dashboardUrlField,
|
|
600
|
+
testId: z.string().describe("Canonical t_ test ID")
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
async ({ dashboardUrl, testId }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`, "DELETE")
|
|
604
|
+
);
|
|
605
|
+
registerAnalyticsTool(
|
|
606
|
+
"agent_qa_list_suites",
|
|
607
|
+
{
|
|
608
|
+
title: "List agent-qa suites",
|
|
609
|
+
description: "List suites through the dashboard API using dashboard workspace discovery.",
|
|
610
|
+
inputSchema: { dashboardUrl: dashboardUrlField }
|
|
611
|
+
},
|
|
612
|
+
async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/suites")
|
|
613
|
+
);
|
|
614
|
+
registerAnalyticsTool(
|
|
615
|
+
"agent_qa_read_suite",
|
|
616
|
+
{
|
|
617
|
+
title: "Read agent-qa suite",
|
|
618
|
+
description: "Read a suite by canonical suite ID through the dashboard API.",
|
|
619
|
+
inputSchema: {
|
|
620
|
+
dashboardUrl: dashboardUrlField,
|
|
621
|
+
suiteId: z.string().describe("Canonical s_ suite ID")
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
async ({ dashboardUrl, suiteId }) => dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(suiteId)}`)
|
|
625
|
+
);
|
|
626
|
+
registerAnalyticsTool(
|
|
627
|
+
"agent_qa_validate_suite",
|
|
628
|
+
{
|
|
629
|
+
title: "Validate agent-qa suite YAML",
|
|
630
|
+
description: "Validate suite YAML through the dashboard suite-file manager.",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
dashboardUrl: dashboardUrlField,
|
|
633
|
+
content: z.string().describe("Suite YAML content")
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
async ({ dashboardUrl, content }) => dashboardTool(dashboardUrl, "/api/suites/validate", "POST", { content })
|
|
637
|
+
);
|
|
638
|
+
registerAnalyticsTool(
|
|
639
|
+
"agent_qa_create_suite",
|
|
640
|
+
{
|
|
641
|
+
title: "Create agent-qa suite",
|
|
642
|
+
description: "Create a suite file through the dashboard workspace-safe suite manager.",
|
|
643
|
+
inputSchema: {
|
|
644
|
+
dashboardUrl: dashboardUrlField,
|
|
645
|
+
path: z.string().describe("Workspace-relative suite path matched by workspace.suiteMatch"),
|
|
646
|
+
content: z.string().describe("Suite YAML content")
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
async ({ dashboardUrl, path, content }) => dashboardTool(dashboardUrl, "/api/suites", "POST", { path, content })
|
|
650
|
+
);
|
|
651
|
+
registerAnalyticsTool(
|
|
652
|
+
"agent_qa_update_suite",
|
|
653
|
+
{
|
|
654
|
+
title: "Update agent-qa suite",
|
|
655
|
+
description: "Update a suite by canonical suite ID through the dashboard API.",
|
|
656
|
+
inputSchema: {
|
|
657
|
+
dashboardUrl: dashboardUrlField,
|
|
658
|
+
suiteId: z.string().describe("Canonical s_ suite ID"),
|
|
659
|
+
content: z.string().describe("Replacement suite YAML content")
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
async ({ dashboardUrl, suiteId, content }) => dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(suiteId)}`, "PUT", { content })
|
|
663
|
+
);
|
|
664
|
+
registerAnalyticsTool(
|
|
665
|
+
"agent_qa_delete_suite",
|
|
666
|
+
{
|
|
667
|
+
title: "Delete agent-qa suite",
|
|
668
|
+
description: "Delete a suite through the dashboard API. Prefer suiteId; path is supported for the dashboard delete route.",
|
|
669
|
+
inputSchema: {
|
|
670
|
+
dashboardUrl: dashboardUrlField,
|
|
671
|
+
suiteId: z.string().optional().describe("Canonical s_ suite ID"),
|
|
672
|
+
path: z.string().optional().describe("Workspace-relative suite path")
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
async ({ dashboardUrl, suiteId, path }) => {
|
|
676
|
+
if (path) return dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(path)}`, "DELETE");
|
|
677
|
+
if (!suiteId) return errorContent("Either suiteId or path is required.");
|
|
678
|
+
let readResult;
|
|
679
|
+
try {
|
|
680
|
+
readResult = await callDashboardApi({
|
|
681
|
+
dashboardUrl: dashboardUrlFor(dashboardUrl),
|
|
682
|
+
path: `/api/suites/${encodeURIComponent(suiteId)}`
|
|
683
|
+
});
|
|
684
|
+
} catch (err) {
|
|
685
|
+
return errorContent(err instanceof Error ? err.message : String(err));
|
|
686
|
+
}
|
|
687
|
+
if (!readResult.ok || !readResult.body || typeof readResult.body !== "object") return jsonContent(readResult);
|
|
688
|
+
const foundPath = readResult.body.path;
|
|
689
|
+
if (typeof foundPath !== "string") return errorContent("Suite read response did not include a path.");
|
|
690
|
+
return dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(foundPath)}`, "DELETE");
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
registerAnalyticsTool(
|
|
694
|
+
"agent_qa_list_hooks",
|
|
695
|
+
{
|
|
696
|
+
title: "List agent-qa hooks",
|
|
697
|
+
description: "List hooks through the dashboard hook registry API.",
|
|
698
|
+
inputSchema: { dashboardUrl: dashboardUrlField }
|
|
699
|
+
},
|
|
700
|
+
async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/hooks")
|
|
701
|
+
);
|
|
702
|
+
registerAnalyticsTool(
|
|
703
|
+
"agent_qa_read_hook",
|
|
704
|
+
{
|
|
705
|
+
title: "Read agent-qa hook",
|
|
706
|
+
description: "Read hook metadata and source by canonical hook ID.",
|
|
707
|
+
inputSchema: {
|
|
708
|
+
dashboardUrl: dashboardUrlField,
|
|
709
|
+
hookId: z.string().describe("Canonical h_ hook ID")
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
async ({ dashboardUrl, hookId }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}`)
|
|
713
|
+
);
|
|
714
|
+
registerAnalyticsTool(
|
|
715
|
+
"agent_qa_create_hook",
|
|
716
|
+
{
|
|
717
|
+
title: "Create agent-qa hook",
|
|
718
|
+
description: "Create a hook through the dashboard hook registry API.",
|
|
719
|
+
inputSchema: {
|
|
720
|
+
dashboardUrl: dashboardUrlField,
|
|
721
|
+
payload: z.record(z.string(), z.unknown()).describe("Dashboard HookMutationRequest payload")
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
async ({ dashboardUrl, payload }) => dashboardTool(dashboardUrl, "/api/hooks", "POST", payload)
|
|
725
|
+
);
|
|
726
|
+
registerAnalyticsTool(
|
|
727
|
+
"agent_qa_update_hook",
|
|
728
|
+
{
|
|
729
|
+
title: "Update agent-qa hook",
|
|
730
|
+
description: "Update a hook through the dashboard hook registry API.",
|
|
731
|
+
inputSchema: {
|
|
732
|
+
dashboardUrl: dashboardUrlField,
|
|
733
|
+
hookId: z.string().describe("Canonical h_ hook ID"),
|
|
734
|
+
payload: z.record(z.string(), z.unknown()).describe("Dashboard HookMutationRequest payload")
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
async ({ dashboardUrl, hookId, payload }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}`, "PUT", payload)
|
|
738
|
+
);
|
|
739
|
+
registerAnalyticsTool(
|
|
740
|
+
"agent_qa_delete_hook",
|
|
741
|
+
{
|
|
742
|
+
title: "Delete agent-qa hook",
|
|
743
|
+
description: "Delete a hook through the dashboard hook registry API.",
|
|
744
|
+
inputSchema: {
|
|
745
|
+
dashboardUrl: dashboardUrlField,
|
|
746
|
+
hookId: z.string().describe("Canonical h_ hook ID"),
|
|
747
|
+
force: z.boolean().optional().describe("Delete even when references exist")
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
async ({ dashboardUrl, hookId, force }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}${force ? "?force=true" : ""}`, "DELETE")
|
|
751
|
+
);
|
|
752
|
+
registerAnalyticsTool(
|
|
753
|
+
"agent_qa_run_hook",
|
|
754
|
+
{
|
|
755
|
+
title: "Run agent-qa hook",
|
|
756
|
+
description: "Run a hook through the dashboard hook execution API.",
|
|
757
|
+
inputSchema: {
|
|
758
|
+
dashboardUrl: dashboardUrlField,
|
|
759
|
+
hookId: z.string().describe("Canonical h_ hook ID"),
|
|
760
|
+
payload: z.record(z.string(), z.unknown()).optional().describe("Dashboard HookRunRequest payload")
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
async ({ dashboardUrl, hookId, payload }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}/run`, "POST", payload ?? {})
|
|
764
|
+
);
|
|
765
|
+
registerAnalyticsTool(
|
|
766
|
+
"agent_qa_enqueue_test_run",
|
|
767
|
+
{
|
|
768
|
+
title: "Enqueue agent-qa test run",
|
|
769
|
+
description: "Queue a test run through the dashboard run trigger API and return the canonical run ID.",
|
|
770
|
+
inputSchema: {
|
|
771
|
+
dashboardUrl: dashboardUrlField,
|
|
772
|
+
file: z.string().optional().describe("Workspace-relative test file path"),
|
|
773
|
+
patterns: z.array(z.string()).optional().describe("Optional test patterns"),
|
|
774
|
+
noCache: z.boolean().optional(),
|
|
775
|
+
noMemory: z.boolean().optional(),
|
|
776
|
+
local: z.boolean().optional()
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
async ({ dashboardUrl, file, patterns, noCache, noMemory, local }) => dashboardTool(dashboardUrl, "/api/runs/trigger", "POST", { file, patterns, noCache, noMemory, local, triggerSource: "mcp" })
|
|
780
|
+
);
|
|
781
|
+
registerAnalyticsTool(
|
|
782
|
+
"agent_qa_enqueue_suite_run",
|
|
783
|
+
{
|
|
784
|
+
title: "Enqueue agent-qa suite run",
|
|
785
|
+
description: "Queue a suite run through the dashboard run trigger API and return parent run context.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
dashboardUrl: dashboardUrlField,
|
|
788
|
+
file: z.string().describe("Workspace-relative suite file path"),
|
|
789
|
+
noCache: z.boolean().optional(),
|
|
790
|
+
noMemory: z.boolean().optional(),
|
|
791
|
+
local: z.boolean().optional()
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
async ({ dashboardUrl, file, noCache, noMemory, local }) => dashboardTool(dashboardUrl, "/api/runs/trigger", "POST", { file, noCache, noMemory, local, triggerSource: "mcp" })
|
|
795
|
+
);
|
|
796
|
+
registerAnalyticsTool(
|
|
797
|
+
"agent_qa_get_run",
|
|
798
|
+
{
|
|
799
|
+
title: "Get agent-qa run",
|
|
800
|
+
description: "Get run detail, steps, attempts, and suite child context through the dashboard API.",
|
|
801
|
+
inputSchema: {
|
|
802
|
+
dashboardUrl: dashboardUrlField,
|
|
803
|
+
runId: z.string().describe("Canonical r_ run ID")
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}`)
|
|
807
|
+
);
|
|
808
|
+
registerAnalyticsTool(
|
|
809
|
+
"agent_qa_get_run_steps",
|
|
810
|
+
{
|
|
811
|
+
title: "Get agent-qa run steps",
|
|
812
|
+
description: "Get run steps through the dashboard API.",
|
|
813
|
+
inputSchema: {
|
|
814
|
+
dashboardUrl: dashboardUrlField,
|
|
815
|
+
runId: z.string().describe("Canonical r_ run ID")
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/steps`)
|
|
819
|
+
);
|
|
820
|
+
registerAnalyticsTool(
|
|
821
|
+
"agent_qa_get_run_logs",
|
|
822
|
+
{
|
|
823
|
+
title: "Get agent-qa run logs",
|
|
824
|
+
description: "Get run log rows through the dashboard API.",
|
|
825
|
+
inputSchema: {
|
|
826
|
+
dashboardUrl: dashboardUrlField,
|
|
827
|
+
runId: z.string().describe("Canonical r_ run ID"),
|
|
828
|
+
stepId: z.string().optional(),
|
|
829
|
+
level: z.string().optional(),
|
|
830
|
+
source: z.string().optional(),
|
|
831
|
+
limit: z.number().int().positive().optional(),
|
|
832
|
+
offset: z.number().int().min(0).optional()
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
async ({ dashboardUrl, runId, stepId, level, source, limit, offset }) => dashboardTool(
|
|
836
|
+
dashboardUrl,
|
|
837
|
+
withQuery(`/api/runs/${encodeURIComponent(runId)}/logs`, { stepId, level, source, limit, offset })
|
|
838
|
+
)
|
|
839
|
+
);
|
|
840
|
+
registerAnalyticsTool(
|
|
841
|
+
"agent_qa_get_run_execution_logs",
|
|
842
|
+
{
|
|
843
|
+
title: "Get agent-qa execution logs",
|
|
844
|
+
description: "Get structured execution logs through the dashboard API.",
|
|
845
|
+
inputSchema: {
|
|
846
|
+
dashboardUrl: dashboardUrlField,
|
|
847
|
+
runId: z.string().describe("Canonical r_ run ID"),
|
|
848
|
+
stepId: z.string().optional(),
|
|
849
|
+
type: z.string().optional()
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
async ({ dashboardUrl, runId, stepId, type }) => dashboardTool(
|
|
853
|
+
dashboardUrl,
|
|
854
|
+
withQuery(`/api/runs/${encodeURIComponent(runId)}/execution-logs`, { stepId, type })
|
|
855
|
+
)
|
|
856
|
+
);
|
|
857
|
+
registerAnalyticsTool(
|
|
858
|
+
"agent_qa_get_run_artifact",
|
|
859
|
+
{
|
|
860
|
+
title: "Get agent-qa run artifact bundle",
|
|
861
|
+
description: "Get sanitized run artifact bundle, child artifacts, and missing artifact sections.",
|
|
862
|
+
inputSchema: {
|
|
863
|
+
dashboardUrl: dashboardUrlField,
|
|
864
|
+
runId: z.string().describe("Canonical r_ run ID")
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/artifact`)
|
|
868
|
+
);
|
|
869
|
+
registerAnalyticsTool(
|
|
870
|
+
"agent_qa_cancel_run",
|
|
871
|
+
{
|
|
872
|
+
title: "Cancel agent-qa run",
|
|
873
|
+
description: "Cancel a pending or running run through the dashboard queue.",
|
|
874
|
+
inputSchema: {
|
|
875
|
+
dashboardUrl: dashboardUrlField,
|
|
876
|
+
runId: z.string().describe("Canonical r_ run ID")
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/cancel`, "POST", {})
|
|
880
|
+
);
|
|
881
|
+
registerAnalyticsTool(
|
|
882
|
+
"agent_qa_classify_failure",
|
|
883
|
+
{
|
|
884
|
+
title: "Classify agent-qa failure",
|
|
885
|
+
description: "Classify a failed run using dashboard run detail, artifacts, logs, execution logs, and recent related runs.",
|
|
886
|
+
inputSchema: {
|
|
887
|
+
dashboardUrl: dashboardUrlField,
|
|
888
|
+
runId: z.string().describe("Canonical r_ run ID")
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
async ({ dashboardUrl, runId }) => {
|
|
892
|
+
try {
|
|
893
|
+
const baseUrl = dashboardUrlFor(dashboardUrl);
|
|
894
|
+
const runDetail = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}` });
|
|
895
|
+
const artifact = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/artifact` });
|
|
896
|
+
const logs = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/logs` });
|
|
897
|
+
const executionLogs = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/execution-logs` });
|
|
898
|
+
const runName = runDetail.body && typeof runDetail.body === "object" && runDetail.body.run && typeof runDetail.body.run?.name === "string" ? runDetail.body.run.name : void 0;
|
|
899
|
+
const recentRuns = runName ? await callDashboardApi({ dashboardUrl: baseUrl, path: withQuery("/api/runs", { name: runName, limit: 5 }) }) : { ok: true, status: 200, body: { runs: [] } };
|
|
900
|
+
return jsonContent({
|
|
901
|
+
runId,
|
|
902
|
+
classification: classifyRunFailureFromDashboardData({
|
|
903
|
+
runDetail: runDetail.body,
|
|
904
|
+
artifact: artifact.body,
|
|
905
|
+
logs: logs.body,
|
|
906
|
+
executionLogs: executionLogs.body,
|
|
907
|
+
recentRuns: recentRuns.body
|
|
908
|
+
}),
|
|
909
|
+
sources: {
|
|
910
|
+
run: { ok: runDetail.ok, status: runDetail.status },
|
|
911
|
+
artifact: { ok: artifact.ok, status: artifact.status },
|
|
912
|
+
logs: { ok: logs.ok, status: logs.status },
|
|
913
|
+
executionLogs: { ok: executionLogs.ok, status: executionLogs.status },
|
|
914
|
+
recentRuns: { ok: recentRuns.ok, status: recentRuns.status }
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
} catch (err) {
|
|
918
|
+
return errorContent(err instanceof Error ? err.message : String(err));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
);
|
|
922
|
+
for (const schema of SCHEMA_REFERENCE_NAMES) {
|
|
923
|
+
server.registerResource(
|
|
924
|
+
`agent_qa.schema.${schema}`,
|
|
925
|
+
`agent-qa://schema/${schema}`,
|
|
926
|
+
{
|
|
927
|
+
title: `agent-qa ${schema} schema reference`,
|
|
928
|
+
description: `Structured agent-qa reference for ${schema}.`,
|
|
929
|
+
mimeType: "application/json"
|
|
930
|
+
},
|
|
931
|
+
async (uri) => ({
|
|
932
|
+
contents: [{
|
|
933
|
+
uri: uri.href,
|
|
934
|
+
mimeType: "application/json",
|
|
935
|
+
text: JSON.stringify(AGENT_QA_SCHEMA_REFERENCES[schema], null, 2)
|
|
936
|
+
}]
|
|
937
|
+
})
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
server.registerPrompt(
|
|
941
|
+
"agent_qa_authoring_context",
|
|
942
|
+
{
|
|
943
|
+
title: "agent-qa authoring context",
|
|
944
|
+
description: "Prompt context for agents writing agent-qa tests and suites.",
|
|
945
|
+
argsSchema: {}
|
|
946
|
+
},
|
|
947
|
+
async () => ({
|
|
948
|
+
messages: [{
|
|
949
|
+
role: "user",
|
|
950
|
+
content: {
|
|
951
|
+
type: "text",
|
|
952
|
+
text: [
|
|
953
|
+
"Use agent-qa MCP tools before editing files.",
|
|
954
|
+
"Call agent_qa_get_config to inspect targets/devices/providers.",
|
|
955
|
+
"Call agent_qa_generate_id for test, suite, hook, run, and observation IDs.",
|
|
956
|
+
"Call agent_qa_validate_definition before saving config, test, suite, or hooks YAML.",
|
|
957
|
+
`Canonical ID contracts: ${JSON.stringify(getEntityIdContracts2())}`
|
|
958
|
+
].join("\n")
|
|
959
|
+
}
|
|
960
|
+
}]
|
|
961
|
+
})
|
|
962
|
+
);
|
|
963
|
+
return server;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/server.ts
|
|
967
|
+
function createMcpServer(options = {}) {
|
|
968
|
+
return createAgentQaMcpServer(options);
|
|
969
|
+
}
|
|
970
|
+
async function startMcpServer(options = {}) {
|
|
971
|
+
const server = createMcpServer({ ...options, transport: "stdio" });
|
|
972
|
+
const transport = new StdioServerTransport();
|
|
973
|
+
await server.connect(transport);
|
|
974
|
+
try {
|
|
975
|
+
if (options.analyticsConfig?.analytics?.privacy !== true) {
|
|
976
|
+
const standardProperties = options.analyticsStandardProperties ?? await resolveAnalyticsStandardProperties2({ surface: "mcp" });
|
|
977
|
+
const event = buildAnalyticsEvent2({
|
|
978
|
+
name: "agent-qa.mcp.server.lifecycle",
|
|
979
|
+
properties: {
|
|
980
|
+
...standardProperties,
|
|
981
|
+
surface: standardProperties.surface ?? "mcp",
|
|
982
|
+
mcp_server_state: "started",
|
|
983
|
+
mcp_transport: "stdio"
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
if (options.analyticsService) {
|
|
987
|
+
await options.analyticsService.capture(event).catch(() => {
|
|
988
|
+
});
|
|
989
|
+
} else {
|
|
990
|
+
await captureAnalytics2(event, {
|
|
991
|
+
config: options.analyticsConfig,
|
|
992
|
+
surface: "mcp"
|
|
993
|
+
}).catch(() => {
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
} catch {
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/local-http.ts
|
|
1002
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1003
|
+
var DEFAULT_MCP_HOST = "127.0.0.1";
|
|
1004
|
+
var DEFAULT_MCP_PORT = 3471;
|
|
1005
|
+
var DEFAULT_MCP_PATH = "/mcp";
|
|
1006
|
+
function normalizePath(path) {
|
|
1007
|
+
if (!path) return DEFAULT_MCP_PATH;
|
|
1008
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
1009
|
+
}
|
|
1010
|
+
function jsonRpcError(res, status, message) {
|
|
1011
|
+
res.writeHead(status, {
|
|
1012
|
+
"Content-Type": "application/json",
|
|
1013
|
+
"Access-Control-Allow-Origin": "*"
|
|
1014
|
+
});
|
|
1015
|
+
res.end(JSON.stringify({
|
|
1016
|
+
jsonrpc: "2.0",
|
|
1017
|
+
error: {
|
|
1018
|
+
code: -32e3,
|
|
1019
|
+
message
|
|
1020
|
+
},
|
|
1021
|
+
id: null
|
|
1022
|
+
}));
|
|
1023
|
+
}
|
|
1024
|
+
function resolveLocalMcpEndpoint(config = {}) {
|
|
1025
|
+
const transport = config.transport ?? "http";
|
|
1026
|
+
const host = config.host ?? DEFAULT_MCP_HOST;
|
|
1027
|
+
const port = config.port ?? DEFAULT_MCP_PORT;
|
|
1028
|
+
const path = normalizePath(config.path);
|
|
1029
|
+
const displayHost = host === "::1" ? "[::1]" : host;
|
|
1030
|
+
return {
|
|
1031
|
+
enabled: config.enabled !== false,
|
|
1032
|
+
transport,
|
|
1033
|
+
host,
|
|
1034
|
+
port,
|
|
1035
|
+
path,
|
|
1036
|
+
url: `http://${displayHost}:${port}${path}`
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function resolveMcpEndpointShape(endpoint) {
|
|
1040
|
+
return {
|
|
1041
|
+
mcp_host_kind: ["127.0.0.1", "localhost", "::1"].includes(endpoint.host) ? "loopback" : "other",
|
|
1042
|
+
mcp_port_kind: endpoint.port === DEFAULT_MCP_PORT ? "default" : "custom",
|
|
1043
|
+
mcp_path_kind: endpoint.path === DEFAULT_MCP_PATH ? "default" : "custom"
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function createLocalMcpHttpHandler(options = {}) {
|
|
1047
|
+
const endpoint = resolveLocalMcpEndpoint(options.endpoint);
|
|
1048
|
+
const baseHost = endpoint.host === "::1" ? "[::1]" : endpoint.host;
|
|
1049
|
+
return async (req, res) => {
|
|
1050
|
+
const requestUrl = new URL(req.url ?? "/", `http://${baseHost}:${endpoint.port}`);
|
|
1051
|
+
if (requestUrl.pathname !== endpoint.path) {
|
|
1052
|
+
jsonRpcError(res, 404, `MCP endpoint is available at ${endpoint.path}`);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (req.method === "OPTIONS") {
|
|
1056
|
+
res.writeHead(204, {
|
|
1057
|
+
"Access-Control-Allow-Origin": "*",
|
|
1058
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
1059
|
+
"Access-Control-Allow-Headers": "Content-Type, MCP-Protocol-Version, mcp-session-id"
|
|
1060
|
+
});
|
|
1061
|
+
res.end();
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const server = createAgentQaMcpServer({
|
|
1065
|
+
configPath: options.configPath,
|
|
1066
|
+
dashboardUrl: options.dashboardUrl,
|
|
1067
|
+
endpointUrl: endpoint.url,
|
|
1068
|
+
analyticsConfig: options.analyticsConfig,
|
|
1069
|
+
transport: "http"
|
|
1070
|
+
});
|
|
1071
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1072
|
+
sessionIdGenerator: void 0
|
|
1073
|
+
});
|
|
1074
|
+
try {
|
|
1075
|
+
await server.connect(transport);
|
|
1076
|
+
await transport.handleRequest(req, res);
|
|
1077
|
+
res.on("close", () => {
|
|
1078
|
+
void transport.close();
|
|
1079
|
+
void server.close();
|
|
1080
|
+
});
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
void transport.close();
|
|
1083
|
+
void server.close();
|
|
1084
|
+
if (!res.headersSent) {
|
|
1085
|
+
jsonRpcError(
|
|
1086
|
+
res,
|
|
1087
|
+
500,
|
|
1088
|
+
err instanceof Error ? err.message : "Internal MCP server error"
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
export {
|
|
1095
|
+
AGENT_QA_SCHEMA_REFERENCES,
|
|
1096
|
+
DEFAULT_MCP_HOST,
|
|
1097
|
+
DEFAULT_MCP_PATH,
|
|
1098
|
+
DEFAULT_MCP_PORT,
|
|
1099
|
+
SCHEMA_REFERENCE_NAMES,
|
|
1100
|
+
VALIDATION_KINDS,
|
|
1101
|
+
classifyRunFailureFromDashboardData,
|
|
1102
|
+
createAgentQaMcpServer,
|
|
1103
|
+
createLocalMcpHttpHandler,
|
|
1104
|
+
createMcpServer,
|
|
1105
|
+
resolveDashboardApiUrl,
|
|
1106
|
+
resolveLocalMcpEndpoint,
|
|
1107
|
+
resolveMcpEndpointShape,
|
|
1108
|
+
startMcpServer,
|
|
1109
|
+
validateAgentQaDefinition
|
|
1110
|
+
};
|
|
1111
|
+
//# sourceMappingURL=index.js.map
|