@vostride/agent-qa-mcp 0.1.21 → 0.2.2

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/dist/index.cjs CHANGED
@@ -20,1122 +20,2189 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- AGENT_QA_SCHEMA_REFERENCES: () => AGENT_QA_SCHEMA_REFERENCES,
24
- DEFAULT_MCP_HOST: () => DEFAULT_MCP_HOST,
25
- DEFAULT_MCP_PATH: () => DEFAULT_MCP_PATH,
26
- DEFAULT_MCP_PORT: () => DEFAULT_MCP_PORT,
27
- MCP_STDIO_STARTUP_MESSAGE: () => MCP_STDIO_STARTUP_MESSAGE,
28
- SCHEMA_REFERENCE_NAMES: () => SCHEMA_REFERENCE_NAMES,
29
- VALIDATION_KINDS: () => VALIDATION_KINDS,
30
- classifyRunFailureFromDashboardData: () => classifyRunFailureFromDashboardData,
31
- createAgentQaMcpServer: () => createAgentQaMcpServer,
32
- createLocalMcpHttpHandler: () => createLocalMcpHttpHandler,
33
- createMcpServer: () => createMcpServer,
34
- resolveDashboardApiUrl: () => resolveDashboardApiUrl,
35
- resolveLocalMcpEndpoint: () => resolveLocalMcpEndpoint,
36
- resolveMcpEndpointShape: () => resolveMcpEndpointShape,
37
- startMcpServer: () => startMcpServer,
38
- validateAgentQaDefinition: () => validateAgentQaDefinition
23
+ AGENT_QA_COMPOSED_CONTRACTS: () => AGENT_QA_COMPOSED_CONTRACTS,
24
+ AGENT_QA_MCP_INSTRUCTIONS: () => AGENT_QA_MCP_INSTRUCTIONS,
25
+ AGENT_QA_MCP_SERVER_VERSION: () => AGENT_QA_MCP_SERVER_VERSION,
26
+ AGENT_QA_OP_CONTRACTS: () => AGENT_QA_OP_CONTRACTS,
27
+ AGENT_QA_TOOL_PROJECTIONS: () => AGENT_QA_TOOL_PROJECTIONS,
28
+ APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY: () => APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY,
29
+ APP_RESOURCE_MIME_TYPE: () => APP_RESOURCE_MIME_TYPE,
30
+ APP_RESOURCE_NAME: () => APP_RESOURCE_NAME,
31
+ APP_RESOURCE_URI_TEMPLATE: () => APP_RESOURCE_URI_TEMPLATE,
32
+ APP_WRAPPER_APP_NAME: () => APP_WRAPPER_APP_NAME,
33
+ APP_WRAPPER_APP_VERSION: () => APP_WRAPPER_APP_VERSION,
34
+ APP_WRAPPER_CONNECTING_HINT: () => APP_WRAPPER_CONNECTING_HINT,
35
+ APP_WRAPPER_CONNECTING_MESSAGE: () => APP_WRAPPER_CONNECTING_MESSAGE,
36
+ APP_WRAPPER_EXPIRED_HINT: () => APP_WRAPPER_EXPIRED_HINT,
37
+ APP_WRAPPER_EXPIRED_MESSAGE: () => APP_WRAPPER_EXPIRED_MESSAGE,
38
+ APP_WRAPPER_LIVE_MESSAGE: () => APP_WRAPPER_LIVE_MESSAGE,
39
+ APP_WRAPPER_LOAD_TIMEOUT_MS: () => APP_WRAPPER_LOAD_TIMEOUT_MS,
40
+ APP_WRAPPER_PERMANENT_LINK_LABEL: () => APP_WRAPPER_PERMANENT_LINK_LABEL,
41
+ APP_WRAPPER_RUN_ENDED_MESSAGE: () => APP_WRAPPER_RUN_ENDED_MESSAGE,
42
+ APP_WRAPPER_STATES: () => APP_WRAPPER_STATES,
43
+ APP_WRAPPER_UI_PROTOCOL_VERSION: () => APP_WRAPPER_UI_PROTOCOL_VERSION,
44
+ APP_WRAPPER_UNREACHABLE_MESSAGE: () => APP_WRAPPER_UNREACHABLE_MESSAGE,
45
+ AppResourceNegotiationError: () => AppResourceNegotiationError,
46
+ AppResourceOriginSpanError: () => AppResourceOriginSpanError,
47
+ AppResourceReadError: () => AppResourceReadError,
48
+ AppResourceUriError: () => AppResourceUriError,
49
+ DEGRADED_OUTPUT_CONTRACTS: () => DEGRADED_OUTPUT_CONTRACTS,
50
+ JOB_STATUS_TO_MCP_TASK_STATE: () => JOB_STATUS_TO_MCP_TASK_STATE,
51
+ LIVE_LINK_ABSENT_LINE: () => LIVE_LINK_ABSENT_LINE,
52
+ LIVE_LINK_NO_RUN_LINE: () => LIVE_LINK_NO_RUN_LINE,
53
+ MCP_APPS_EXTENSION_ID: () => MCP_APPS_EXTENSION_ID,
54
+ McpProjectionError: () => McpProjectionError,
55
+ UNPROJECTED_OPS: () => UNPROJECTED_OPS,
56
+ V1_TOOL_MIGRATION_MAP: () => V1_TOOL_MIGRATION_MAP,
57
+ advertisedContracts: () => advertisedContracts,
58
+ appResourceUriFor: () => appResourceUriFor,
59
+ armWithoutDiscriminant: () => armWithoutDiscriminant,
60
+ buildAgentQaMcpServer: () => buildAgentQaMcpServer,
61
+ compositionOf: () => compositionOf,
62
+ createAgentQaMcpNodeHandler: () => createAgentQaMcpNodeHandler,
63
+ createAppResourceReadHandler: () => createAppResourceReadHandler,
64
+ foldArmFindings: () => foldArmFindings,
65
+ liveLinkLine: () => liveLinkLine,
66
+ liveLinkLineFor: () => liveLinkLineFor,
67
+ liveRunLinkOf: () => liveRunLinkOf,
68
+ maskedToolResult: () => maskedToolResult,
69
+ negotiatedAppsExtension: () => negotiatedAppsExtension,
70
+ parseAppUri: () => parseAppUri,
71
+ projectJobAsTask: () => projectJobAsTask,
72
+ registerAppResources: () => registerAppResources,
73
+ renderAppWrapperPage: () => renderAppWrapperPage,
74
+ stripRedundantObjectStamp: () => stripRedundantObjectStamp,
75
+ toRegisterToolConfig: () => toRegisterToolConfig,
76
+ withoutDialect: () => withoutDialect,
77
+ withoutLegacyResultWrap: () => withoutLegacyResultWrap
39
78
  });
40
79
  module.exports = __toCommonJS(index_exports);
41
80
 
42
- // src/server.ts
43
- var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
44
- var import_agent_qa_core3 = require("@vostride/agent-qa-core");
81
+ // src/catalog.ts
82
+ var import_server3 = require("@modelcontextprotocol/server");
83
+ var import_agent_qa_agent3 = require("@vostride/agent-qa-agent");
45
84
 
46
- // src/agent-qa-server.ts
47
- var import_promises = require("fs/promises");
48
- var import_node_path = require("path");
49
- var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
50
- var import_agent_qa_core2 = require("@vostride/agent-qa-core");
51
- var import_agent_qa_ids2 = require("@vostride/agent-qa-ids");
52
- var import_yaml = require("yaml");
53
- var import_zod = require("zod");
85
+ // src/app-resource.ts
86
+ var import_server = require("@modelcontextprotocol/server");
87
+ var import_agent_qa_agent = require("@vostride/agent-qa-agent");
88
+ var import_agent_qa_schema = require("@vostride/agent-qa-schema");
54
89
 
55
- // src/schema-reference.ts
56
- var import_agent_qa_core = require("@vostride/agent-qa-core");
57
- var import_agent_qa_ids = require("@vostride/agent-qa-ids");
58
- var SCHEMA_REFERENCE_NAMES = ["config", "test", "suite", "hooks", "ids"];
59
- var VALIDATION_KINDS = ["config", "test", "suite", "hooks"];
60
- var AGENT_QA_SCHEMA_REFERENCES = {
61
- config: {
62
- name: "agent-qa config",
63
- rootKeys: ["workspace", "services", "registry", "use", "analytics"],
64
- services: {
65
- dashboard: ["port", "dbPath", "artifactsDir"],
66
- mcp: ["enabled", "transport", "host", "port", "path"],
67
- cache: ["dir", "ttl"],
68
- logging: ["level"],
69
- recording: ["enabled"],
70
- accessibility: ["enabled", "standard", "runAfter", "failOnViolation", "disableRules", "exclude"],
71
- memory: ["enabled", "provider", "dir", "minTrust", "maxInjections", "curatorEnabled"]
72
- },
73
- mcpLocalOnly: {
74
- transports: ["http", "stdio"],
75
- hosts: ["localhost", "127.0.0.1", "::1"],
76
- defaultPort: 3471,
77
- defaultPath: "/mcp"
78
- }
79
- },
80
- test: {
81
- name: "agent-qa test YAML",
82
- requiredKeys: ["test-id", "name", "target", "steps"],
83
- optionalKeys: ["context", "use", "meta", "setup", "teardown"],
84
- idType: "test",
85
- hookReferences: "setup and teardown arrays must contain canonical h_ hook IDs.",
86
- stepFormats: ["string", "object with step/timeout/retries/screenshot/capture/maxAttempts"]
87
- },
88
- suite: {
89
- name: "agent-qa suite YAML",
90
- requiredKeys: ["name", "target", "tests"],
91
- optionalKeys: ["suite-id", "context", "setup", "teardown", "use"],
92
- idType: "suite",
93
- testReference: "Each tests[] entry uses { test: path, id: canonical t_ test ID }.",
94
- hookReferences: "setup and teardown arrays must contain canonical h_ hook IDs."
95
- },
96
- hooks: {
97
- name: "agent-qa hooks file",
98
- rootKeys: ["hooks"],
99
- hookKeys: ["id", "name", "runtime", "file", "deps", "packageFile", "timeout", "network"],
100
- idType: "hook",
101
- runtimes: ["node", "bun", "python", "bash"]
102
- },
103
- ids: {
104
- name: "agent-qa canonical IDs",
105
- instruction: "Generate IDs with agent_qa_generate_id or `agent-qa ids generate <type>`; never hand-write IDs.",
106
- contracts: (0, import_agent_qa_ids.getEntityIdContracts)()
107
- }
90
+ // src/app-wrapper.ts
91
+ var APP_WRAPPER_STATES = [
92
+ "connecting",
93
+ "live",
94
+ "expired",
95
+ "unreachable-origin",
96
+ "run-ended"
97
+ ];
98
+ var APP_WRAPPER_CONNECTING_MESSAGE = "connecting to the live view\u2026";
99
+ var APP_WRAPPER_CONNECTING_HINT = "if the pane does not appear, open it in your browser";
100
+ var APP_WRAPPER_LIVE_MESSAGE = "live";
101
+ var APP_WRAPPER_EXPIRED_MESSAGE = "live link expired";
102
+ var APP_WRAPPER_EXPIRED_HINT = "ask to watch again \u2014 a fresh watch mints a new link";
103
+ var APP_WRAPPER_UNREACHABLE_MESSAGE = "this host can't reach the live view \u2014 open it in your browser";
104
+ var APP_WRAPPER_RUN_ENDED_MESSAGE = "this run is not live \u2014 open it in your browser";
105
+ var APP_WRAPPER_PERMANENT_LINK_LABEL = "open in your browser";
106
+ var APP_WRAPPER_LOAD_TIMEOUT_MS = 1e4;
107
+ var APP_WRAPPER_UI_PROTOCOL_VERSION = "2026-01-26";
108
+ var APP_WRAPPER_APP_NAME = "agent-qa-live-view";
109
+ var APP_WRAPPER_APP_VERSION = "1.0.0";
110
+ var STYLE = `
111
+ :root { color-scheme: dark light }
112
+ html, body { margin: 0; height: 100%; background: #0b0d10; }
113
+ main { display: grid; grid-template-rows: auto 1fr auto auto; height: 100%; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; color: #c9d1d9; }
114
+ header { display: flex; gap: .75rem; align-items: center; padding: .5rem .75rem; }
115
+ header:empty { display: none; }
116
+ iframe { border: 0; width: 100%; height: 100%; display: none; background: #0b0d10; }
117
+ main[data-app-state="live"] iframe { display: block; }
118
+ main[data-app-state="live"] p[data-app-message] { display: none; }
119
+ main[data-app-state="live"] p[data-app-hint] { display: none; }
120
+ p[data-app-message] { margin: 0; padding: .5rem .75rem; }
121
+ p[data-app-hint] { margin: 0; padding: 0 .75rem .5rem; opacity: .75; }
122
+ p[data-app-hint]:empty { display: none; }
123
+ footer { padding: .5rem .75rem; }
124
+ select[hidden], a[hidden] { display: none; }
125
+ `;
126
+ var MESSAGES = {
127
+ connecting: APP_WRAPPER_CONNECTING_MESSAGE,
128
+ live: APP_WRAPPER_LIVE_MESSAGE,
129
+ expired: APP_WRAPPER_EXPIRED_MESSAGE,
130
+ "unreachable-origin": APP_WRAPPER_UNREACHABLE_MESSAGE,
131
+ "run-ended": APP_WRAPPER_RUN_ENDED_MESSAGE
108
132
  };
109
- var VALIDATION_SCHEMAS = {
110
- config: import_agent_qa_core.AgentQaConfigSchema,
111
- test: import_agent_qa_core.TestDefinitionSchema,
112
- suite: import_agent_qa_core.SuiteDefinitionSchema,
113
- hooks: import_agent_qa_core.HooksFileSchema
133
+ var HINTS = {
134
+ connecting: APP_WRAPPER_CONNECTING_HINT,
135
+ live: "",
136
+ expired: APP_WRAPPER_EXPIRED_HINT,
137
+ "unreachable-origin": "",
138
+ "run-ended": ""
114
139
  };
115
- function validateAgentQaDefinition(kind, definition) {
116
- const result = VALIDATION_SCHEMAS[kind].safeParse(definition);
117
- if (result.success) {
118
- return {
119
- valid: true,
120
- kind,
121
- issues: []
122
- };
140
+ function httpPermanentLink(value) {
141
+ if (typeof value !== "string") return void 0;
142
+ let parsed;
143
+ try {
144
+ parsed = new URL(value);
145
+ } catch {
146
+ return void 0;
123
147
  }
124
- return {
125
- valid: false,
126
- kind,
127
- issues: result.error.issues.map((issue) => ({
128
- path: issue.path.map(String).join(".") || "(root)",
129
- message: issue.message
130
- }))
148
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? value : void 0;
149
+ }
150
+ function renderAppWrapperPage(input) {
151
+ const scriptLiteral = (value) => JSON.stringify(value).replaceAll("<", "\\u003c");
152
+ const attributeText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
153
+ const now = input.now ?? Date.now();
154
+ const live = input.runLive && input.mints.length > 0;
155
+ const mints = live ? input.mints : [];
156
+ const lowestExpiry = mints.reduce(
157
+ (lowest, mint) => mint.expiresAt < lowest ? mint.expiresAt : lowest,
158
+ Number.POSITIVE_INFINITY
159
+ );
160
+ const expired = live && lowestExpiry <= now;
161
+ const expiresInMs = expired || !live ? 0 : Math.max(0, lowestExpiry - now);
162
+ const initialState = !live ? "run-ended" : expired ? "expired" : "connecting";
163
+ const mintsLiteral = (expired ? [] : mints).map(
164
+ (mint) => `{handle:${scriptLiteral(String(mint.handle))},url:${scriptLiteral(mint.liveUrl)},kind:${scriptLiteral(mint.streamKind)}}`
165
+ ).join(",");
166
+ const permanentLink = httpPermanentLink(input.permanentLink);
167
+ const permanentLinkSlot = permanentLink === void 0 ? "" : '<a data-app-permanent-link rel="noreferrer" target="_blank" href="' + attributeText(permanentLink) + '">' + attributeText(APP_WRAPPER_PERMANENT_LINK_LABEL) + "</a>";
168
+ return `<!DOCTYPE html>
169
+ <html lang="en">
170
+ <head>
171
+ <meta charset="utf-8">
172
+ <meta name="viewport" content="width=device-width, initial-scale=1">
173
+ <title>agent-qa live run</title>
174
+ <style>${STYLE}</style>
175
+ </head>
176
+ <body>
177
+ <main data-app-state="${initialState}">
178
+ <header><select data-app-handle-picker hidden></select></header>
179
+ ${live && !expired ? '<iframe data-app-live-frame sandbox="allow-scripts" referrerpolicy="no-referrer" title="agent-qa live view"></iframe>' : ""}
180
+ <p data-app-message>${MESSAGES[initialState]}</p>
181
+ <p data-app-hint>${HINTS[initialState]}</p>
182
+ <footer>${permanentLinkSlot}</footer>
183
+ </main>
184
+ <script>
185
+ (function () {
186
+ var mints = [${mintsLiteral}];
187
+ var runId = ${scriptLiteral(input.runId)};
188
+ var expiresInMs = ${String(expiresInMs)};
189
+ var loadTimeoutMs = ${String(APP_WRAPPER_LOAD_TIMEOUT_MS)};
190
+ var messages = {
191
+ connecting: ${scriptLiteral(APP_WRAPPER_CONNECTING_MESSAGE)},
192
+ live: ${scriptLiteral(APP_WRAPPER_LIVE_MESSAGE)},
193
+ expired: ${scriptLiteral(APP_WRAPPER_EXPIRED_MESSAGE)},
194
+ 'unreachable-origin': ${scriptLiteral(APP_WRAPPER_UNREACHABLE_MESSAGE)},
195
+ 'run-ended': ${scriptLiteral(APP_WRAPPER_RUN_ENDED_MESSAGE)}
196
+ };
197
+ var hints = {
198
+ connecting: ${scriptLiteral(APP_WRAPPER_CONNECTING_HINT)},
199
+ live: '',
200
+ expired: ${scriptLiteral(APP_WRAPPER_EXPIRED_HINT)},
201
+ 'unreachable-origin': '',
202
+ 'run-ended': ''
131
203
  };
204
+
205
+ var root = document.querySelector('[data-app-state]');
206
+ var frame = document.querySelector('[data-app-live-frame]');
207
+ var picker = document.querySelector('[data-app-handle-picker]');
208
+ var message = document.querySelector('[data-app-message]');
209
+ var hint = document.querySelector('[data-app-hint]');
210
+
211
+ // WHICH RUN, as an attribute rather than as prose. Identity is not run STATE (D-11 forbids the
212
+ // second, not the first), and it enters through setAttribute from an escaped literal, so a run
213
+ // id can never become markup no matter what it spells.
214
+ root.setAttribute('data-app-run-id', runId);
215
+
216
+ // The state machine is an ATTRIBUTE plus two textContent writes, exactly as the embed page's is:
217
+ // a state a person can read off the DOM is a state a cell can assert without screenshotting, and
218
+ // text that arrives through textContent can never become markup.
219
+ function setState(state) {
220
+ root.setAttribute('data-app-state', state);
221
+ message.textContent = messages[state] || '';
222
+ hint.textContent = hints[state] || '';
223
+ }
224
+
225
+ // ---- The minimal ui/initialize handshake (ext-apps spec shape) -------------------------------
226
+ //
227
+ // HAND-ROLLED AND DELIBERATELY ISOLATED IN THIS ONE BLOCK. The ext-apps package
228
+ // declares a peer on the v1 monolith SDK, so importing its app-bridge into production would drag
229
+ // a second SDK generation in (Pitfall 3) \u2014 and this page has no bundler anyway. RESEARCH
230
+ // assumption A5 rated the method-name and field spellings MEDIUM (single spec fetch), and plan
231
+ // 02's coverage entry D9 deferred the adjudication to plan 06.
232
+ //
233
+ // PLAN 06 MEASURED IT, AND IT HAD DRIFTED. Against the installed
234
+ // @modelcontextprotocol/ext-apps@1.7.5's own generated schema, McpUiInitializeRequest.params
235
+ // requires THREE members: protocolVersion, appInfo and appCapabilities. This block sent only the
236
+ // first, so a host validating the request against the published schema would have rejected the
237
+ // handshake outright, and the failure would have looked like a host that simply does not support
238
+ // Apps. The two missing members are now sent, and ext-apps-constants.test.ts cell (4d) derives
239
+ // the required list FROM the shipped schema rather than restating it, so the next spec change
240
+ // reds here instead of on a user's machine.
241
+ //
242
+ // IT STILL ASKS FOR NOTHING. appCapabilities is deliberately empty: this document exposes no
243
+ // tools, requests no display mode and declares no experimental feature (the schema requires no
244
+ // member of it). No permissions, no re-mint (D-10) \u2014 it announces that this document is ready,
245
+ // which is all a wrapper that renders no run state of its own has to say.
246
+ try {
247
+ if (window.parent !== window) {
248
+ window.parent.postMessage({
249
+ jsonrpc: '2.0',
250
+ id: 'ui-init-1',
251
+ method: 'ui/initialize',
252
+ params: {
253
+ protocolVersion: ${scriptLiteral(APP_WRAPPER_UI_PROTOCOL_VERSION)},
254
+ appInfo: { name: ${scriptLiteral(APP_WRAPPER_APP_NAME)}, version: ${scriptLiteral(APP_WRAPPER_APP_VERSION)} },
255
+ appCapabilities: {}
256
+ }
257
+ }, '*');
258
+ }
259
+ } catch (e) { /* a host that refuses the message changes nothing about what this page shows */ }
260
+
261
+ if (mints.length === 0) {
262
+ // Either D-07's honest terminal/unknown state or D-10's expired one \u2014 both were decided by the
263
+ // RENDERER and are already in the markup. No src was ever assigned, so there is no blank frame
264
+ // to mistake for a stalled one, and nothing below this line can move the document off them.
265
+ return;
266
+ }
267
+
268
+ // ---- Reachability, once per ORIGIN (see the header's measured note) ---------------------------
269
+ //
270
+ // Memoised deliberately, and ONLY ON SUCCESS. The picker swaps among panes that share one
271
+ // origin, so after the first ANSWERING pane every swap is served from this map and issues NO
272
+ // network request at all (D-06/D-10's zero re-mints, zero calls, which the DOM cell counts) \u2014 a
273
+ // property that rides entirely on caching the true. The catch arm therefore FORGETS the entry
274
+ // before answering false: a refusal is a fact about one moment, not about the origin, and every
275
+ // mint on a render shares one origin, so a cached false was total and permanent. See the header.
276
+ var reach = {};
277
+ function reachable(url) {
278
+ var origin;
279
+ try { origin = new URL(url).origin; } catch (e) { return Promise.resolve(false); }
280
+ if (reach[origin] === undefined) {
281
+ reach[origin] = fetch(origin + '/', { mode: 'no-cors', cache: 'no-store' })
282
+ .then(function () { return true; })
283
+ .catch(function () { delete reach[origin]; return false; });
284
+ }
285
+ return reach[origin];
286
+ }
287
+
288
+ var attachId = 0;
289
+
290
+ function attach(mint) {
291
+ // THE EXPIRED STATE IS TERMINAL, AND THIS IS THE ONE PLACE IT COULD BE LEFT (D-10). The
292
+ // picker's change handler \u2014 and nothing else \u2014 can reach this function after the one-shot
293
+ // expiry timer has fired; without this line the next statements would set the connecting state
294
+ // and assign an aged token over a document that had already told the truth, and no timer would
295
+ // ever correct it. The identical predicate inside settle() stays where it is: it closes the
296
+ // OTHER door, a probe or a frame load that was already in flight when the timer fired, so
297
+ // neither guard makes the other dead.
298
+ if (root.getAttribute('data-app-state') === 'expired') return;
299
+ var mine = (attachId += 1);
300
+ var loaded = false;
301
+ var settled = false;
302
+
303
+ function settle(state) {
304
+ if (settled || mine !== attachId) return;
305
+ if (root.getAttribute('data-app-state') === 'expired') return;
306
+ settled = true;
307
+ setState(state);
308
+ }
309
+
310
+ setState('connecting');
311
+ // THE ONLY PLACE A TOKEN IS WRITTEN. It goes into the frame's src and nowhere else.
312
+ frame.src = mint.url;
313
+
314
+ frame.addEventListener('error', function () { settle('unreachable-origin'); }, { once: true });
315
+ frame.addEventListener('load', function () {
316
+ // NECESSARY, NOT SUFFICIENT: Chromium fires this for a refused connection too (header note),
317
+ // so the probe is what decides \u2014 a frame that loaded on an origin nothing answered for is an
318
+ // error page, and calling that 'live' is the failure this arm exists to prevent.
319
+ loaded = true;
320
+ reachable(mint.url).then(function (ok) { settle(ok ? 'live' : 'unreachable-origin'); });
321
+ }, { once: true });
322
+
323
+ reachable(mint.url).then(function (ok) {
324
+ if (!ok) settle('unreachable-origin');
325
+ else if (loaded) settle('live');
326
+ });
327
+
328
+ // D-11's REQUIRED arm for the origin that never answers at all: a black-holed host fires no
329
+ // error and never loads, so silence past this bound is itself the signal, named rather than
330
+ // left as a spinner.
331
+ setTimeout(function () { settle('unreachable-origin'); }, loadTimeoutMs);
332
+ }
333
+
334
+ // D-10: the deadline was computed at RENDER time and arrives as a delta, so the host's own clock
335
+ // never enters the comparison. When it fires the document says so and stops \u2014 nothing here mints.
336
+ if (expiresInMs > 0) {
337
+ setTimeout(function () {
338
+ attachId += 1;
339
+ // Unconditional: the script returned above when mints was empty, and a non-empty mints is
340
+ // exactly the condition under which the renderer emits the iframe, so there is no reachable
341
+ // state in which this callback runs without a frame to strip.
342
+ frame.removeAttribute('src');
343
+ // The picker is an affordance of the EMBEDDED variant; when the variant is gone the
344
+ // affordance goes with it, so the control a person can reach and the state the document is in
345
+ // cannot disagree.
346
+ picker.setAttribute('disabled', '');
347
+ setState('expired');
348
+ }, expiresInMs);
349
+ }
350
+
351
+ // D-06/D-10: the picker swaps among tokens ALREADY minted for this render. It never mints, and
352
+ // there is no bridge here that could ask anyone to \u2014 a fresh watch call is what re-mints.
353
+ for (var i = 0; i < mints.length; i += 1) {
354
+ var option = document.createElement('option');
355
+ option.value = String(i);
356
+ option.textContent = 'handle ' + mints[i].handle;
357
+ picker.appendChild(option);
358
+ }
359
+ if (mints.length > 1) picker.removeAttribute('hidden');
360
+ picker.addEventListener('change', function () {
361
+ var chosen = mints[Number(picker.value)];
362
+ if (chosen !== undefined) attach(chosen);
363
+ });
364
+
365
+ attach(mints[0]);
366
+ }());
367
+ </script>
368
+ </body>
369
+ </html>
370
+ `;
371
+ }
372
+
373
+ // src/live-link.ts
374
+ function liveRunLinkOf(base, runId) {
375
+ return `${base.replace(/\/+$/u, "")}/runs/${encodeURIComponent(runId)}/live`;
376
+ }
377
+ function liveLinkLine(url) {
378
+ return `watch live or review: ${url}`;
379
+ }
380
+ var LIVE_LINK_ABSENT_LINE = "no dashboard configured \u2014 set services.dashboard.baseUrl";
381
+ var LIVE_LINK_NO_RUN_LINE = "no run to link to \u2014 this result names no run id";
382
+ function liveLinkLineFor(base, runId) {
383
+ if (base === void 0 || base.trim() === "") return LIVE_LINK_ABSENT_LINE;
384
+ if (runId === void 0 || runId === "") return LIVE_LINK_NO_RUN_LINE;
385
+ return liveLinkLine(liveRunLinkOf(base.trim(), runId));
132
386
  }
133
387
 
134
- // src/agent-qa-server.ts
135
- var DISCOVERY = {
136
- name: "agent-qa",
137
- purpose: "Author, validate, run, and triage agent-qa tests through local trusted MCP.",
138
- tools: [
139
- "agent_qa_discover",
140
- "agent_qa_get_config",
141
- "agent_qa_schema_reference",
142
- "agent_qa_validate_definition",
143
- "agent_qa_generate_id",
144
- "agent_qa_validate_id",
145
- "agent_qa_list_tests",
146
- "agent_qa_read_test",
147
- "agent_qa_validate_test",
148
- "agent_qa_create_test",
149
- "agent_qa_update_test",
150
- "agent_qa_delete_test",
151
- "agent_qa_list_suites",
152
- "agent_qa_read_suite",
153
- "agent_qa_validate_suite",
154
- "agent_qa_create_suite",
155
- "agent_qa_update_suite",
156
- "agent_qa_delete_suite",
157
- "agent_qa_list_hooks",
158
- "agent_qa_read_hook",
159
- "agent_qa_create_hook",
160
- "agent_qa_update_hook",
161
- "agent_qa_delete_hook",
162
- "agent_qa_run_hook",
163
- "agent_qa_enqueue_test_run",
164
- "agent_qa_enqueue_suite_run",
165
- "agent_qa_get_run",
166
- "agent_qa_get_run_steps",
167
- "agent_qa_get_run_logs",
168
- "agent_qa_get_run_execution_logs",
169
- "agent_qa_get_run_artifact",
170
- "agent_qa_cancel_run",
171
- "agent_qa_classify_failure"
172
- ],
173
- resources: SCHEMA_REFERENCE_NAMES.map((name) => `agent-qa://schema/${name}`),
174
- prompts: ["agent_qa_authoring_context"],
175
- phases: {
176
- current: "discovery-schema-id-contracts",
177
- next: ["authoring tools", "run execution and triage tools"]
388
+ // src/app-resource.ts
389
+ var MCP_APPS_EXTENSION_ID = "io.modelcontextprotocol/ui";
390
+ var APP_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
391
+ var APP_RESOURCE_URI_TEMPLATE = "ui://agent-qa/live-run/{runId}";
392
+ function appResourceUriFor(runId) {
393
+ return APP_RESOURCE_URI_TEMPLATE.replace("{runId}", encodeURIComponent(runId));
394
+ }
395
+ var APP_RESOURCE_NAME = "agent_qa_live_run_app";
396
+ var APP_RESOURCE_TITLE = "agent-qa live run";
397
+ var APP_RESOURCE_DESCRIPTION = "The live view for one run, as an MCP App: a sandboxed page embedding the run's live preview for each member handle. Rendered fresh on every read.";
398
+ var APP_URI_PREFIX = "ui://agent-qa/live-run/";
399
+ function parseAppUri(href) {
400
+ if (!href.startsWith(APP_URI_PREFIX)) return void 0;
401
+ const tail = href.slice(APP_URI_PREFIX.length);
402
+ if (tail === "") return void 0;
403
+ let decoded;
404
+ try {
405
+ decoded = decodeURIComponent(tail);
406
+ } catch {
407
+ return void 0;
408
+ }
409
+ const parsed = import_agent_qa_schema.RunsLiveUrlInputSchema.safeParse({ runId: decoded });
410
+ if (!parsed.success) return void 0;
411
+ return { runId: parsed.data.runId };
412
+ }
413
+ var APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY = import_server.CLIENT_CAPABILITIES_META_KEY;
414
+ function negotiatedAppsExtension(ctx) {
415
+ const envelope = ctx?.mcpReq?.envelope;
416
+ if (envelope === void 0) return false;
417
+ const capabilities = envelope[APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY];
418
+ const extensions = capabilities?.extensions;
419
+ if (extensions === void 0) return false;
420
+ return Object.hasOwn(extensions, MCP_APPS_EXTENSION_ID);
421
+ }
422
+ var AppResourceNegotiationError = class extends Error {
423
+ constructor() {
424
+ super(
425
+ `this resource is an MCP App: it is served only to a client that declared the ${MCP_APPS_EXTENSION_ID} extension in its capabilities`
426
+ );
427
+ this.name = "AppResourceNegotiationError";
178
428
  }
179
429
  };
180
- function jsonContent(value) {
181
- return {
182
- content: [{
183
- type: "text",
184
- text: JSON.stringify(value, null, 2)
185
- }]
186
- };
430
+ var AppResourceUriError = class extends Error {
431
+ constructor(detail) {
432
+ super(`${detail} is not a canonical ui://agent-qa/live-run address`);
433
+ this.name = "AppResourceUriError";
434
+ }
435
+ };
436
+ var AppResourceReadError = class extends Error {
437
+ constructor(message) {
438
+ super(message);
439
+ this.name = "AppResourceReadError";
440
+ }
441
+ };
442
+ var AppResourceOriginSpanError = class extends Error {
443
+ constructor(origins) {
444
+ super(
445
+ `this App document would span ${origins.length} origins (${[...origins].sort().join(", ")}) and one frame allowance covers one: refusing to serve a document whose panes it could not all reach`
446
+ );
447
+ this.name = "AppResourceOriginSpanError";
448
+ }
449
+ };
450
+ function isNotLiveRefusal(error) {
451
+ return error instanceof import_agent_qa_agent.UnknownRunError || error instanceof import_agent_qa_agent.LiveViewUnavailableError || error instanceof import_agent_qa_agent.FunctionNotBoundError;
187
452
  }
188
- function errorContent(message) {
189
- return {
190
- isError: true,
191
- content: [{
192
- type: "text",
193
- text: message
194
- }]
453
+ async function announcedRoster(invoke, runId) {
454
+ const stream = await invoke(import_agent_qa_agent.RUNS_EVENTS_FUNCTION_ID, { runId });
455
+ const iterable = stream;
456
+ if (iterable === void 0 || typeof iterable[Symbol.asyncIterator] !== "function") {
457
+ return void 0;
458
+ }
459
+ const iterator = iterable[Symbol.asyncIterator]();
460
+ try {
461
+ const first = await iterator.next();
462
+ if (first.done === true) return void 0;
463
+ return (0, import_agent_qa_schema.handleRosterOf)(first.value);
464
+ } finally {
465
+ await iterator.return?.(void 0);
466
+ }
467
+ }
468
+ function createAppResourceReadHandler(deps) {
469
+ return async (uri, _variables, ctx) => {
470
+ if (!negotiatedAppsExtension(ctx)) throw new AppResourceNegotiationError();
471
+ const parsed = parseAppUri(uri.href);
472
+ if (parsed === void 0) {
473
+ throw new AppResourceUriError(deps.maskText(uri.href));
474
+ }
475
+ const { runId } = parsed;
476
+ let roster;
477
+ let notLive = false;
478
+ try {
479
+ roster = await announcedRoster(deps.invoke, runId);
480
+ } catch (err) {
481
+ if (!isNotLiveRefusal(err)) {
482
+ throw new AppResourceReadError(
483
+ deps.maskText(err instanceof Error ? err.message : String(err))
484
+ );
485
+ }
486
+ notLive = true;
487
+ }
488
+ const rosterToMint = roster === void 0 || roster.length === 0 ? [0] : roster;
489
+ const mints = [];
490
+ if (!notLive) {
491
+ for (const handle of rosterToMint) {
492
+ let minted;
493
+ try {
494
+ minted = await deps.invoke(import_agent_qa_agent.RUNS_LIVE_URL_FUNCTION_ID, {
495
+ runId,
496
+ handle
497
+ });
498
+ } catch (err) {
499
+ if (!isNotLiveRefusal(err)) {
500
+ throw new AppResourceReadError(
501
+ deps.maskText(err instanceof Error ? err.message : String(err))
502
+ );
503
+ }
504
+ continue;
505
+ }
506
+ mints.push({
507
+ handle: minted.handle,
508
+ liveUrl: minted.liveUrl,
509
+ expiresAt: minted.expiresAt,
510
+ streamKind: minted.stream.kind
511
+ });
512
+ }
513
+ }
514
+ const base = deps.liveLink?.dashboardBaseUrl();
515
+ const permanentLink = base === void 0 || base.trim() === "" ? void 0 : liveRunLinkOf(base.trim(), runId);
516
+ const frameOrigins = [
517
+ ...new Set(
518
+ mints.map((mint) => originOf(mint.liveUrl)).filter((origin) => origin !== void 0)
519
+ )
520
+ ];
521
+ if (frameOrigins.length > 1) throw new AppResourceOriginSpanError(frameOrigins);
522
+ const frameDomain = frameOrigins[0];
523
+ const html = renderAppWrapperPage({
524
+ runId,
525
+ mints,
526
+ permanentLink,
527
+ runLive: mints.length > 0
528
+ });
529
+ return {
530
+ contents: [
531
+ {
532
+ // Re-formatted through the codec's own vocabulary rather than echoed, so the address a
533
+ // caller reads back is the canonical one.
534
+ uri: `${APP_URI_PREFIX}${runId}`,
535
+ mimeType: APP_RESOURCE_MIME_TYPE,
536
+ text: html,
537
+ // The host's default is `frame-src 'none'`, so an App that names no frame domain paints
538
+ // a BLOCKED inner frame — the blank pane criterion 2 forbids (Pitfall 1). The allowed
539
+ // origin is the minted link's OWN origin rather than a separately configured one, so the
540
+ // allow can never be broader than the document needs (T-408-05). `frameDomain` is the ONE
541
+ // distinct origin `frameOrigins` above resolved over EVERY mint (IN-02) — not `mints[0]`'s,
542
+ // which was the same answer only by a fact held in another package.
543
+ //
544
+ // TWO ABSENCES, NOT ONE (corrected 2026-09-01, 408 gap round; `408-VERIFICATION.md` W-4 /
545
+ // `408-REVIEW.md` WR-03). This comment used to read "with no mints there is no inner
546
+ // frame, and the block is omitted rather than emitted empty", which describes only the
547
+ // FIRST of two conditions that both reach `frameDomain === undefined`. They are different
548
+ // questions with different served documents, and collapsing them told a reader about a
549
+ // state the code does not produce:
550
+ // - `mints.length` asks whether there is anything to render at all. Zero mints means
551
+ // `runLive` is `false`, the wrapper emits no `<iframe data-app-live-frame>`, and the
552
+ // omitted `csp` block allows nothing because nothing needs allowing. Honest.
553
+ // - `frameOrigins[0]` is the SEPARATE question of whether any surviving mint yielded a
554
+ // parseable origin to put in `_meta.ui.csp`. With `mints.length > 0` and every
555
+ // `originOf(mint.liveUrl)` answering `undefined`, `runLive` is `true` — the document
556
+ // DOES emit the iframe and DOES assign `frame.src` — while the `csp` block is omitted,
557
+ // so a conformant host falls back to `frame-src 'none'` and the pane is blocked and
558
+ // blank. Not honest, and the sentence above never mentioned it.
559
+ // WHY NO SHIPPED PORT REACHES IT: `runs.liveurl` would have to answer a `liveUrl` that
560
+ // is not a parseable URL, and no shipped port does. The verifier measured this as
561
+ // unreachable and classified W-4 as a documentation finding rather than a gap; the
562
+ // wrapper's probe/timeout arm would in any case settle `unreachable-origin` with the
563
+ // permanent link rather than leave a blank pane under a progress claim, so SC2 survives
564
+ // even on the unreachable path.
565
+ //
566
+ // THE INVARIANT IS UNASSERTED, AND THAT IS A DATED DEFERRAL RATHER THAN A SILENCE
567
+ // (2026-09-01, 408 gap round). "Every mint that survives into `runLive` has a nameable
568
+ // origin" is asserted nowhere today — not here, not in `renderAppWrapperPage`, not in a
569
+ // cell. It is the natural candidate and it is NOT taken in this round, because this round
570
+ // is a gap-closure round whose whole reported gap is documentation integrity.
571
+ // THE RULE THAT MAKES THAT CONSISTENT, because the neighbouring round looks
572
+ // contradictory and a reader is entitled to it: THIS ROUND CHANGES WHAT THE SYSTEM
573
+ // SAYS, NEVER WHAT IT DOES. 408-16 task 1 does edit a runtime file — `config.ts` — but
574
+ // it only adds words to an error that was already being thrown on exactly the inputs it
575
+ // was already thrown on; no parse outcome moves in either direction and no input that
576
+ // succeeded now fails. Asserting the invariant here would add a NEW failure mode to a
577
+ // path that currently succeeds. Operator-facing error prose is in scope because it is
578
+ // prose; a new runtime invariant is out of scope because it is behaviour. That is the
579
+ // whole distinction, and it is what makes W-2 closable in this round and W-4's
580
+ // invariant half not.
581
+ //
582
+ // AND THE NEW PROSE CENSUS DOES NOT COVER THIS, which is the question the gap brief asks
583
+ // by name. `__tests__/withdrawn-claim-census.test.ts` gates ONE withdrawn security claim
584
+ // about the token channel, keyed on a channel cue bound to an exclusivity cue; W-4 is a
585
+ // different species — a comment that CONFLATES two conditions rather than one that
586
+ // over-claims a control — and widening that census to "any comment that conflates two
587
+ // conditions" is not mechanizable, and a vocabulary that tried would red on honest prose,
588
+ // which this round's own prohibition forbids.
589
+ //
590
+ // `connectDomains` NAMES THE SAME ONE ORIGIN, AND IT IS NOT OPTIONAL (408 plan 06 Task 2,
591
+ // MEASURED 2026-08-31). ext-apps' shipped schema documents `connectDomains` as mapping to
592
+ // `connect-src` with "empty or omitted -> no network connections (secure default)", and
593
+ // 408-02 D-H gave this wrapper a no-cors probe of the frame's origin — because Chromium
594
+ // fires `load` for a REFUSED connection, so `load` alone would label an error page `live`.
595
+ // With only `frameDomains` declared, a conformant host serves the App under
596
+ // `connect-src 'none'`, the probe is blocked before it leaves the document, and the
597
+ // wrapper settles in `unreachable-origin` for a live view that is perfectly reachable —
598
+ // measured in `app-host-harness.test.ts`, with Chromium naming the directive in its own
599
+ // console. That is not a blank frame, but it is a lie with a sentence attached, which is
600
+ // worse: it tells a person to go somewhere else while the pixels are right there.
601
+ //
602
+ // It is exactly as narrow as the frame allowance — the same single minted origin, never a
603
+ // wildcard and never a second configured value — so the App can reach the one host it
604
+ // embeds and nothing else.
605
+ ...frameDomain === void 0 ? {} : {
606
+ _meta: {
607
+ ui: { csp: { frameDomains: [frameDomain], connectDomains: [frameDomain] } }
608
+ }
609
+ }
610
+ }
611
+ ]
612
+ };
195
613
  };
196
614
  }
197
- function categorizeMcpTool(name) {
198
- if (name === "agent_qa_discover") return "discovery";
199
- if (name.includes("schema") || name.includes("definition") || name.includes("config")) return "schema";
200
- if (name.includes("_id")) return "id";
201
- if (name.includes("_hook")) return "hook";
202
- if (name.includes("classify")) return "triage";
203
- if (name.includes("_run") || name.includes("_logs") || name.includes("_artifact") || name.includes("_fix") || name.includes("_failure")) return "run";
204
- if (name.includes("_test") || name.includes("_suite")) return "authoring";
205
- return "unknown";
615
+ function originOf(liveUrl) {
616
+ try {
617
+ return new URL(liveUrl).origin;
618
+ } catch {
619
+ return void 0;
620
+ }
621
+ }
622
+ function registerAppResources(server, deps) {
623
+ const declared = deps.registry.list(deps.caller);
624
+ const mint = declared.find((candidate) => candidate.id === import_agent_qa_agent.RUNS_LIVE_URL_FUNCTION_ID);
625
+ const roster = declared.find((candidate) => candidate.id === import_agent_qa_agent.RUNS_EVENTS_FUNCTION_ID);
626
+ if (mint === void 0 || roster === void 0) return [];
627
+ if (mint.bindingStatus !== "bound") return [];
628
+ const read = createAppResourceReadHandler({
629
+ // The wall, bound once: every mint crosses `invokeFunction` with this host's registry and this
630
+ // caller, so input validation and the binding check run exactly as they do for a tool call.
631
+ // There is no store, no cache and no second data path.
632
+ invoke: (opId, input) => (0, import_agent_qa_agent.invokeFunction)(deps.registry, opId, input, deps.caller),
633
+ maskText: deps.maskText,
634
+ // Absent stays ABSENT rather than becoming a present `undefined` supplier: the house rule for
635
+ // an optional member, and the two are the same posture to the read either way.
636
+ ...deps.liveLink === void 0 ? {} : { liveLink: deps.liveLink }
637
+ });
638
+ server.registerResource(
639
+ APP_RESOURCE_NAME,
640
+ // `list: undefined` is REQUIRED to be spelled out by the SDK, and here the explicit absence is
641
+ // load-bearing rather than merely honest: an unlisted template cannot be reached by
642
+ // enumeration, so no model walking `resources/list` can discover an address whose read mints a
643
+ // credential (Pitfall 2). Enumerating every watchable run is a listing this surface does not
644
+ // offer, and this is the line that says so.
645
+ new import_server.ResourceTemplate(APP_RESOURCE_URI_TEMPLATE, { list: void 0 }),
646
+ {
647
+ title: APP_RESOURCE_TITLE,
648
+ description: APP_RESOURCE_DESCRIPTION,
649
+ mimeType: APP_RESOURCE_MIME_TYPE
650
+ },
651
+ read
652
+ );
653
+ return [APP_RESOURCE_URI_TEMPLATE];
206
654
  }
207
- function isErrorResult(value) {
208
- return Boolean(value && typeof value === "object" && value.isError === true);
655
+
656
+ // src/instructions.ts
657
+ var AGENT_QA_MCP_INSTRUCTIONS = [
658
+ "This project is tested with agent-qa. Read its runs and knowledge only through the agent_qa_* tools on this server, or the agent-qa CLI with --json.",
659
+ "Never open .agent-qa/, SQLite files, agent-qa-knowledge/memory/index/, memory/queue/, .lease or .epoch, and never list, glob or search those folders: they are agent-qa's runtime state.",
660
+ "Start with agent_qa_list_runs, then agent_qa_get_run_overview for one run.",
661
+ "The knowledge guide is agent-qa-knowledge/AGENTS.md; `npx agent-qa skills` lists the skills and their folder."
662
+ ].join("\n");
663
+
664
+ // src/mapping.ts
665
+ var import_agent_qa_schema2 = require("@vostride/agent-qa-schema");
666
+ var MAX_RESULT_SIZE_META_KEY = "anthropic/maxResultSizeChars";
667
+ var RUN_READ_MAX_RESULT_SIZE_CHARS = 1e5;
668
+ function compositionOf(projection) {
669
+ if (projection.ops.length === 1) return "single";
670
+ return projection.discriminant === void 0 ? "pipeline" : "union";
209
671
  }
210
- function isDashboardErrorResult(value) {
211
- const parsed = parseToolJsonContent(value);
212
- return Boolean(parsed && typeof parsed === "object" && parsed.ok === false);
672
+ var AGENT_QA_OP_CONTRACTS = Object.freeze({
673
+ "runs.mark": Object.freeze({ input: import_agent_qa_schema2.RunsMarkInputSchema, output: import_agent_qa_schema2.RunsMarkOutcomeSchema }),
674
+ // 2026-09-15, 423-21 D-18: the feedback reads keep their ops' own strict contracts. The preview
675
+ // takes runs.mark's exact input and answers the read-only plan result.
676
+ "runs.marks.history": Object.freeze({
677
+ input: import_agent_qa_schema2.RunsMarksHistoryInputSchema,
678
+ output: import_agent_qa_schema2.RunsMarksHistoryResultSchema
679
+ }),
680
+ "runs.mark.preview": Object.freeze({ input: import_agent_qa_schema2.RunsMarkInputSchema, output: import_agent_qa_schema2.RunsMarkPreviewResultSchema }),
681
+ "knowledge.gates.list": Object.freeze({
682
+ input: import_agent_qa_schema2.KnowledgeGatesListInputSchema,
683
+ output: import_agent_qa_schema2.KnowledgeGatesListResultSchema
684
+ }),
685
+ "authoring.draft.write": Object.freeze({
686
+ input: import_agent_qa_schema2.AuthoringDraftWriteInputSchema,
687
+ output: import_agent_qa_schema2.AuthoringDraftWriteOutputSchema
688
+ }),
689
+ "authoring.draft.edit": Object.freeze({
690
+ input: import_agent_qa_schema2.AuthoringDraftEditInputSchema,
691
+ output: import_agent_qa_schema2.AuthoringDraftEditOutputSchema
692
+ }),
693
+ "authoring.draft.verify": Object.freeze({
694
+ input: import_agent_qa_schema2.AuthoringVerifyDraftInputSchema,
695
+ output: import_agent_qa_schema2.AuthoringVerifyDraftOutputSchema
696
+ }),
697
+ "runs.start": Object.freeze({
698
+ input: import_agent_qa_schema2.RunsStartInputSchema,
699
+ output: import_agent_qa_schema2.RunStartAcknowledgementSchema
700
+ }),
701
+ "config.field.set": Object.freeze({
702
+ input: import_agent_qa_schema2.ConfigFieldSetInputSchema,
703
+ output: import_agent_qa_schema2.ConfigChangeOutputSchema
704
+ }),
705
+ "config.target.create": Object.freeze({
706
+ input: import_agent_qa_schema2.ConfigTargetCreateInputSchema,
707
+ output: import_agent_qa_schema2.ConfigChangeOutputSchema
708
+ }),
709
+ "config.target.edit": Object.freeze({
710
+ input: import_agent_qa_schema2.ConfigTargetEditInputSchema,
711
+ output: import_agent_qa_schema2.ConfigChangeOutputSchema
712
+ }),
713
+ "config.auth.set": Object.freeze({
714
+ input: import_agent_qa_schema2.ConfigAuthSetInputSchema,
715
+ output: import_agent_qa_schema2.ConfigChangeOutputSchema
716
+ }),
717
+ "config.hook.add": Object.freeze({
718
+ input: import_agent_qa_schema2.ConfigHookAddInputSchema,
719
+ output: import_agent_qa_schema2.ConfigChangeOutputSchema
720
+ }),
721
+ "config.project.scaffold": Object.freeze({
722
+ input: import_agent_qa_schema2.ConfigProjectScaffoldInputSchema,
723
+ output: import_agent_qa_schema2.ConfigChangeOutputSchema
724
+ }),
725
+ // 2026-09-30 (432-12 D-05): the three knowledge changes answer what they applied.
726
+ "knowledge.accept": Object.freeze({
727
+ input: import_agent_qa_schema2.KnowledgeAcceptChangeInputSchema,
728
+ output: import_agent_qa_schema2.KnowledgeChangeOutputSchema
729
+ }),
730
+ "knowledge.reject": Object.freeze({
731
+ input: import_agent_qa_schema2.KnowledgeRejectChangeInputSchema,
732
+ output: import_agent_qa_schema2.KnowledgeChangeOutputSchema
733
+ }),
734
+ "knowledge.forget": Object.freeze({
735
+ input: import_agent_qa_schema2.KnowledgeForgetChangeInputSchema,
736
+ output: import_agent_qa_schema2.KnowledgeChangeOutputSchema
737
+ }),
738
+ "entities.resolve": Object.freeze({
739
+ input: import_agent_qa_schema2.EntitiesResolveInputSchema,
740
+ output: import_agent_qa_schema2.EntityCardResolutionSchema
741
+ }),
742
+ "entities.get": Object.freeze({
743
+ input: import_agent_qa_schema2.EntitiesGetInputSchema,
744
+ output: import_agent_qa_schema2.EntityCardSchema
745
+ }),
746
+ // 2026-09-28 (431-03, D-03/D-07): the canonical list contract — `RunsListOutputSchema` as the
747
+ // validator, the same object `runs.list` enforces with, plus its explicit output document.
748
+ "runs.list": Object.freeze({
749
+ input: import_agent_qa_schema2.RunsListInputSchema,
750
+ output: import_agent_qa_schema2.RunsListOutputContract
751
+ }),
752
+ "runs.digest": Object.freeze({
753
+ input: import_agent_qa_schema2.RunsDigestInputSchema,
754
+ output: import_agent_qa_schema2.RunDigestSchema
755
+ }),
756
+ // 2026-09-28 (431-04 D-04/D-05/D-07): the eight reads keep their ops' own contracts. The
757
+ // evidence arms use the canonical output contracts, whose validators are the runtime schemas.
758
+ "knowledge.bundles.list": Object.freeze({
759
+ input: import_agent_qa_schema2.KnowledgeBundlesListInputSchema,
760
+ output: import_agent_qa_schema2.KnowledgeBundlesListOutputSchema
761
+ }),
762
+ "knowledge.records.inspect": Object.freeze({
763
+ input: import_agent_qa_schema2.KnowledgeRecordsInspectInputSchema,
764
+ output: import_agent_qa_schema2.KnowledgeRecordsInspectOutputSchema
765
+ }),
766
+ "knowledge.records.diff": Object.freeze({
767
+ input: import_agent_qa_schema2.KnowledgeRecordsDiffInputSchema,
768
+ output: import_agent_qa_schema2.KnowledgeRecordsDiffOutputSchema
769
+ }),
770
+ "knowledge.record.show": Object.freeze({
771
+ input: import_agent_qa_schema2.KnowledgeRecordShowInputSchema,
772
+ output: import_agent_qa_schema2.KnowledgeRecordShowResultSchema
773
+ }),
774
+ "knowledge.brief.why": Object.freeze({
775
+ input: import_agent_qa_schema2.BriefWhyInputSchema,
776
+ output: import_agent_qa_schema2.BriefWhyResultSchema
777
+ }),
778
+ "runs.knowledge": Object.freeze({
779
+ input: import_agent_qa_schema2.RunsKnowledgeInputSchema,
780
+ output: import_agent_qa_schema2.RunKnowledgeProjectionSchema
781
+ }),
782
+ "runs.overview": Object.freeze({
783
+ input: import_agent_qa_schema2.RunsOverviewInputSchema,
784
+ output: import_agent_qa_schema2.RunOverviewProjectionSchema
785
+ }),
786
+ "runs.evidence": Object.freeze({
787
+ input: import_agent_qa_schema2.RunsEvidenceInputSchema,
788
+ output: import_agent_qa_schema2.RunsEvidenceOutputContract
789
+ }),
790
+ "runs.artifact": Object.freeze({
791
+ input: import_agent_qa_schema2.RunsArtifactInputSchema,
792
+ output: import_agent_qa_schema2.RunsArtifactOutputContract
793
+ })
794
+ });
795
+ var AGENT_QA_TOOL_PROJECTIONS = Object.freeze([
796
+ Object.freeze({
797
+ name: "agent_qa_write_test",
798
+ ops: Object.freeze(["authoring.draft.write"]),
799
+ kind: "workflow"
800
+ }),
801
+ Object.freeze({
802
+ name: "agent_qa_refine_test",
803
+ ops: Object.freeze(["authoring.draft.edit", "authoring.draft.verify"]),
804
+ kind: "workflow",
805
+ discriminant: import_agent_qa_schema2.TOOL_FOLD_DISCRIMINANT
806
+ }),
807
+ Object.freeze({
808
+ name: "agent_qa_start_run",
809
+ ops: Object.freeze(["runs.start"]),
810
+ kind: "workflow",
811
+ // 408 D-05: a run starts and is immediately watchable. The ack carries the runId, so the
812
+ // per-call reference is derived from the OUTPUT.
813
+ ui: Object.freeze({ resource: "live-run", runIdFrom: "output" })
814
+ }),
815
+ // 2026-09-30 (432-12 D-05): the config change tool writes in the call. The undo arm is gone.
816
+ Object.freeze({
817
+ name: "agent_qa_change_config",
818
+ ops: Object.freeze([
819
+ "config.field.set",
820
+ "config.target.create",
821
+ "config.target.edit",
822
+ "config.auth.set",
823
+ "config.hook.add",
824
+ "config.project.scaffold"
825
+ ]),
826
+ kind: "workflow",
827
+ discriminant: import_agent_qa_schema2.TOOL_FOLD_DISCRIMINANT
828
+ }),
829
+ // 2026-09-30 (432-12 D-05/D-09): accept, reject and forget apply in the call. Forget is the
830
+ // explicit removal; a wrong record is otherwise corrected by new feedback.
831
+ Object.freeze({
832
+ name: "agent_qa_change_knowledge",
833
+ ops: Object.freeze([
834
+ "knowledge.accept",
835
+ "knowledge.reject",
836
+ "knowledge.forget"
837
+ ]),
838
+ kind: "workflow",
839
+ discriminant: import_agent_qa_schema2.TOOL_FOLD_DISCRIMINANT
840
+ }),
841
+ Object.freeze({
842
+ name: "agent_qa_mark_run",
843
+ ops: Object.freeze(["runs.mark"]),
844
+ kind: "apply"
845
+ }),
846
+ // 2026-09-15, 423-21 D-18: marks history and the mark consequence preview beside the mark
847
+ // writer. Both are read projections with no apply envelope, so `agent_qa_mark_run` stays the
848
+ // only door that lands a mark.
849
+ Object.freeze({
850
+ name: "agent_qa_get_run_marks",
851
+ ops: Object.freeze(["runs.marks.history"]),
852
+ kind: "read"
853
+ }),
854
+ Object.freeze({
855
+ name: "agent_qa_preview_run_mark",
856
+ ops: Object.freeze(["runs.mark.preview"]),
857
+ kind: "read"
858
+ }),
859
+ Object.freeze({
860
+ name: "agent_qa_list_knowledge_gates",
861
+ ops: Object.freeze(["knowledge.gates.list"]),
862
+ kind: "read"
863
+ }),
864
+ // 2026-09-28, 431 D-04: the knowledge reads, named one-to-one with their CLI verbs.
865
+ Object.freeze({
866
+ name: "agent_qa_list_knowledge_bundles",
867
+ ops: Object.freeze(["knowledge.bundles.list"]),
868
+ kind: "read"
869
+ }),
870
+ Object.freeze({
871
+ name: "agent_qa_inspect_knowledge_records",
872
+ ops: Object.freeze(["knowledge.records.inspect"]),
873
+ kind: "read"
874
+ }),
875
+ Object.freeze({
876
+ name: "agent_qa_diff_knowledge_records",
877
+ ops: Object.freeze(["knowledge.records.diff"]),
878
+ kind: "read"
879
+ }),
880
+ Object.freeze({
881
+ name: "agent_qa_show_knowledge_record",
882
+ ops: Object.freeze(["knowledge.record.show"]),
883
+ kind: "read"
884
+ }),
885
+ Object.freeze({
886
+ name: "agent_qa_explain_brief",
887
+ ops: Object.freeze(["knowledge.brief.why"]),
888
+ kind: "read"
889
+ }),
890
+ Object.freeze({
891
+ name: "agent_qa_get_entity",
892
+ ops: Object.freeze(["entities.resolve", "entities.get"]),
893
+ kind: "read"
894
+ }),
895
+ Object.freeze({
896
+ name: "agent_qa_list_runs",
897
+ ops: Object.freeze(["runs.list"]),
898
+ kind: "read"
899
+ }),
900
+ Object.freeze({
901
+ name: "agent_qa_get_run_digest",
902
+ ops: Object.freeze(["runs.digest"]),
903
+ kind: "read",
904
+ // 408 D-05/D-07: the watch/status read, and therefore the LATE-ATTACH door — a run started
905
+ // from the CLI, from CI or from the dashboard is watchable through it exactly as an
906
+ // MCP-started one is, because a viewer attaches and never owns. Its runId is on the INPUT.
907
+ ui: Object.freeze({ resource: "live-run", runIdFrom: "input" })
908
+ }),
909
+ // 2026-09-28, 431 D-05/D-06/D-07: one completed run's whole reads. The overview is the map, run
910
+ // knowledge the whole used-and-learned projection unchanged, and evidence one addressed document
911
+ // or blob. All three carry the result-size hint.
912
+ Object.freeze({
913
+ name: "agent_qa_get_run_overview",
914
+ ops: Object.freeze(["runs.overview"]),
915
+ kind: "read",
916
+ maxResultSizeChars: RUN_READ_MAX_RESULT_SIZE_CHARS
917
+ }),
918
+ Object.freeze({
919
+ name: "agent_qa_get_run_knowledge",
920
+ ops: Object.freeze(["runs.knowledge"]),
921
+ kind: "read",
922
+ maxResultSizeChars: RUN_READ_MAX_RESULT_SIZE_CHARS
923
+ }),
924
+ Object.freeze({
925
+ name: "agent_qa_get_run_evidence",
926
+ ops: Object.freeze(["runs.evidence", "runs.artifact"]),
927
+ kind: "read",
928
+ discriminant: import_agent_qa_schema2.TOOL_FOLD_DISCRIMINANT,
929
+ maxResultSizeChars: RUN_READ_MAX_RESULT_SIZE_CHARS
930
+ })
931
+ ]);
932
+ var AGENT_QA_COMPOSED_CONTRACTS = Object.freeze({
933
+ agent_qa_refine_test: Object.freeze({
934
+ input: import_agent_qa_schema2.AuthoringRefineFoldInputSchema,
935
+ output: import_agent_qa_schema2.AuthoringRefineFoldOutputSchema
936
+ }),
937
+ agent_qa_change_config: Object.freeze({
938
+ input: import_agent_qa_schema2.ConfigChangeFoldInputSchema,
939
+ output: import_agent_qa_schema2.ConfigChangeFoldOutputSchema
940
+ }),
941
+ agent_qa_change_knowledge: Object.freeze({
942
+ input: import_agent_qa_schema2.KnowledgeChangeFoldInputSchema,
943
+ output: import_agent_qa_schema2.KnowledgeChangeFoldOutputSchema
944
+ }),
945
+ // 2026-09-28 (431-04 D-07): the evidence fold, composed in the schema package from both arms'
946
+ // canonical contracts and routed by full op id.
947
+ agent_qa_get_run_evidence: Object.freeze({
948
+ input: import_agent_qa_schema2.RunsEvidenceFoldInputSchema,
949
+ output: import_agent_qa_schema2.RunsEvidenceFoldOutputContract
950
+ }),
951
+ agent_qa_get_entity: Object.freeze({
952
+ input: import_agent_qa_schema2.EntitiesResolveInputSchema,
953
+ output: import_agent_qa_schema2.EntityCardResolutionSchema
954
+ })
955
+ });
956
+ function advertisedContracts(projection) {
957
+ const composed = AGENT_QA_COMPOSED_CONTRACTS[projection.name];
958
+ if (composed !== void 0) return composed;
959
+ const [only] = projection.ops;
960
+ return only === void 0 ? void 0 : AGENT_QA_OP_CONTRACTS[only];
213
961
  }
214
- function parseToolJsonContent(value) {
215
- if (!value || typeof value !== "object") return void 0;
216
- const content = value.content;
217
- if (!Array.isArray(content)) return void 0;
218
- const firstText = content.find(
219
- (item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string"
220
- );
221
- if (!firstText) return void 0;
962
+ var DEGRADED_OUTPUT_CONTRACTS = Object.freeze([
963
+ // 397 DISPOSITION (2026-08-16 · D-20): keep-degraded. `agent_qa_start_run` advertises no
964
+ // `outputSchema` and keeps it that way. The cause is a property of the CONTRACT, not of this
965
+ // surface — a branded run id is `z.string().transform(asRunId)`, and the converter refuses a
966
+ // transform because there is no true JSON-Schema for "and then this function runs". The two ways
967
+ // out are both worse: advertising the input-mode document is the second-recipe lie D-16 forbids,
968
+ // and un-branding the id to please a converter would spend a type-safety property of the whole
969
+ // codebase on one surface's listing. 397 rules the degradation ADJUDICATED and CLOSED rather than
970
+ // pending; re-opening it means changing the contract, which is a golden-diff-visible act. The
971
+ // absence itself is not re-pinned here — `projection.ts` probes the conversion both ways and the
972
+ // shipped projection suite owns that fact (RESEARCH Pitfall 9); the conformance suite asserts
973
+ // only the join.
974
+ Object.freeze({
975
+ tool: "agent_qa_start_run",
976
+ opId: "runs.start",
977
+ schema: "RunStartAcknowledgementSchema",
978
+ measured: "Transforms cannot be represented in JSON Schema",
979
+ adjudication: "397"
980
+ })
981
+ // 2026-09-28 (431-03, D-03/D-07 · research A1): the `agent_qa_list_runs` row is REMOVED, not
982
+ // re-ruled. Its objection stands — no hand-written array schema is advertised. What changed is the
983
+ // contract: `runs.list` now declares `RunsListOutputContract`, whose validator is the same
984
+ // `RunsListOutputSchema` object and whose output document is derived from the runtime schema's
985
+ // own output conversion. It converts cleanly, so the probe in `projection.ts` would refuse the row.
986
+ ]);
987
+ var UNPROJECTED_ADJUDICATIONS = Object.freeze(["397", "398", "405", "408", "428"]);
988
+ var UNPROJECTED_OPS = Object.freeze([
989
+ Object.freeze({
990
+ opId: "agent.ask",
991
+ reason: "D-03 (dated 2026-08-15): ask-over-MCP is nested-agent delegation \u2014 trust, token-driven LLM spend, lease interaction and streaming are four separate problems and no MCPS requirement asks for any of them. Deferred as its own seam, not omitted by oversight.",
992
+ adjudication: "397"
993
+ }),
994
+ // 2026-10-01 (432-17, D-05): `proposals.accept` and `proposals.reject` (397, requiresHumanOrigin)
995
+ // are deleted with the inbox and leave the scoping note.
996
+ // 2026-10-01 (432-18, D-14/D-15): `grants.revoke` (397, requiresHumanOrigin) and `grants.list`
997
+ // (397, not a mid-flight read) are deleted with the grant ledger and leave the scoping note.
998
+ Object.freeze({
999
+ opId: "runs.get",
1000
+ reason: "not a mid-flight workflow read per D-02 \u2014 the entity door renders a run's card and the digest carries what triage needs; 397 adjudicates",
1001
+ adjudication: "397"
1002
+ }),
1003
+ Object.freeze({
1004
+ opId: "runs.events",
1005
+ reason: "not a mid-flight workflow read per D-02, and a stream op besides: a `tools/call` is one round trip and has no shape for a resumable event feed; 397 adjudicates",
1006
+ adjudication: "397"
1007
+ }),
1008
+ Object.freeze({
1009
+ opId: "runs.postprocessing",
1010
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1011
+ adjudication: "397"
1012
+ }),
1013
+ Object.freeze({
1014
+ opId: "runs.detail",
1015
+ reason: "D-03 (dated 2026-08-27, 405-01): not a mid-flight workflow read per D-02 \u2014 this is a COMPLETED run's projected step tree, read by the durable run-detail page, and the entity door already renders a run's card while the digest carries what triage needs. The whole projection of a run is also a large one-shot payload with no shape a `tools/call` round trip wants. Deferred as its own seam, not omitted.",
1016
+ adjudication: "405"
1017
+ }),
1018
+ Object.freeze({
1019
+ opId: "runs.liveurl",
1020
+ reason: "D-03 (dated 2026-08-25, 403-01): the op MINTS A CREDENTIAL \u2014 a short-lived, view-scoped capability token for a running browser. Whether an agent-origin MCP caller may mint one, and what it may hand it to, is Phase 408's question (the MCP App embed); projecting it here would answer that question by accident. Deferred as its own seam, not omitted. ANSWERED (dated 2026-08-31, 408 D-01): the deferral above is discharged, and the ruling is INTERNAL MINT ONLY. The MCP server invokes this op BY REFERENCE, inside its App-rendering path and nowhere else, and the minted token lands only in the host-rendered HTML of the unlisted `ui://agent-qa/live-run/{runId}` App resource \u2014 never in tool-result text a model reads. Projecting a mint tool stays REJECTED: a live credential in model context is the T-395-R5-10 shape, and masking it would leave a tool that hands back nothing a host could use. The op therefore STAYS unprojected BY RULING rather than by deferral \u2014 answered on the record, never silently reclassified. RE-GROUNDED (dated 2026-09-01, 408 gap round, road `demote`; the full ruling is in `app-resource.ts`'s header). The answer above STANDS and one half of its stated property is corrected rather than deleted. Whether the document this server serves is host-rendered HTML or text a model consumes is a property of the CALLER, not of this server, so the \"never in tool-result text a model reads\" half over-claimed. What this server guarantees is narrower and it is verifiable: it writes the token into exactly one place, the frame src of the document the `ui://` read serves, and that door is reachable by any caller of a composition whose mint is bound (dated 2026-10-01, 432-20/432-21, D-12: the scope gate that once also bounded it is deleted for every caller, and the loopback bind and the Host/Origin guards still bound who can reach it). The op STAYS unprojected on exactly the same footing, and the reason is unchanged and STRONGER rather than weaker: a projected mint tool would put the credential on a channel a model consumes BY DEFAULT, on every call, for every caller, whereas the `ui://` read is an unlisted address a caller must already know and deliberately ask for.",
1021
+ adjudication: "408"
1022
+ }),
1023
+ Object.freeze({
1024
+ opId: "editor.liveurl",
1025
+ reason: "D-07 (dated 2026-09-25, 428): the op MINTS A CREDENTIAL \u2014 a view credential for a live EDITOR session, which is private to the dashboard page that owns it; it is reachable only through the dashboard door and never through an MCP tool or the MCP App live-run resource. Projecting it would give an agent a watch carrier for a draft execution, which the owner declined in D-07. Unprojected by ruling, not by deferral.",
1026
+ adjudication: "428"
1027
+ }),
1028
+ Object.freeze({
1029
+ opId: "config.get",
1030
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1031
+ adjudication: "397"
1032
+ }),
1033
+ Object.freeze({
1034
+ opId: "analysis.get",
1035
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1036
+ adjudication: "397"
1037
+ }),
1038
+ Object.freeze({
1039
+ opId: "authoring.funnel.get",
1040
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1041
+ adjudication: "397"
1042
+ }),
1043
+ Object.freeze({
1044
+ opId: "quarantine.add",
1045
+ reason: "not a mid-flight workflow write per D-02; 397 adjudicates",
1046
+ adjudication: "397"
1047
+ }),
1048
+ Object.freeze({
1049
+ opId: "quarantine.release",
1050
+ reason: "not a mid-flight workflow write per D-02; 397 adjudicates",
1051
+ adjudication: "397"
1052
+ }),
1053
+ Object.freeze({
1054
+ opId: "quarantine.list",
1055
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1056
+ adjudication: "397"
1057
+ }),
1058
+ // 398 D-09 (dated 2026-08-17): the twentieth entry, and the FIRST op ever dispositioned here that
1059
+ // 397 did not rule on — because it did not exist then. Its exclusion is argued rather than scoped,
1060
+ // which is why it does not take the mid-flight line: `agent.explain` answers "what can you do",
1061
+ // and 397 already ruled the v1 discovery tool NOT a gap on this surface, because the protocol
1062
+ // answers machine discovery itself. Projecting it anyway would be the third answer that ruling
1063
+ // refuses.
1064
+ Object.freeze({
1065
+ opId: "agent.explain",
1066
+ reason: "D-09 (dated 2026-08-17): the protocol answers machine discovery itself \u2014 `tools/list` plus SEP-2575 `server/discover` \u2014 and 398's `init --agent` manifest answers agent discovery from the committed catalog. This op answers a PERSON asking in prose, which is a third question with a third audience. 397 ruled the v1 discovery TOOL not-a-gap on exactly this reasoning; projecting the op would reinstate it under a new name.",
1067
+ adjudication: "398"
1068
+ })
1069
+ // 2026-09-30 (432-12 D-05): the five pre-432 proposal ops went off the catalogue with their
1070
+ // tools. 2026-10-01 (432-17 D-05): they are deleted from the registry and leave the scoping note.
1071
+ ]);
1072
+ var PROJECTED_RULING_ADDENDA = Object.freeze([
1073
+ Object.freeze({
1074
+ opId: "knowledge.bundles.list",
1075
+ ruledBy: "397",
1076
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1077
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_list_knowledge_bundles`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1078
+ }),
1079
+ Object.freeze({
1080
+ opId: "knowledge.records.inspect",
1081
+ ruledBy: "397",
1082
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1083
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_inspect_knowledge_records`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1084
+ }),
1085
+ Object.freeze({
1086
+ opId: "knowledge.records.diff",
1087
+ ruledBy: "397",
1088
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1089
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_diff_knowledge_records`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1090
+ }),
1091
+ Object.freeze({
1092
+ opId: "knowledge.record.show",
1093
+ ruledBy: "397",
1094
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1095
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_show_knowledge_record`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1096
+ }),
1097
+ Object.freeze({
1098
+ opId: "knowledge.brief.why",
1099
+ ruledBy: "397",
1100
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1101
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_explain_brief`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1102
+ }),
1103
+ Object.freeze({
1104
+ opId: "runs.knowledge",
1105
+ ruledBy: "407",
1106
+ ruling: "D-05 (dated 2026-08-29, 407-01): not a mid-flight workflow read per D-02 \u2014 this is a COMPLETED run's used-and-learned projection, read by the durable Knowledge tab, and it is unprojected on exactly the footing its sibling `runs.detail` is. It carries every step's three served brief channels, each one's serve-time retrieval trace and the truncated candidate ledger beside it, so it is a large one-shot payload with no shape a `tools/call` round trip wants \u2014 larger than the step tree, not smaller. A planner mid-flight also has no use for what a FINISHED run was briefed with; the entity door already renders a record's card and the digest carries what triage needs. Deferred as its own seam, not omitted.",
1107
+ addendum: "D-05 (dated 2026-09-28, 431-04): projected as `agent_qa_get_run_knowledge`, answering the whole `runs.knowledge` projection unchanged; the owner declined a step filter. The size objection above is answered in these terms: `agent_qa_get_run_overview` is the entry point that maps a run without bodies, `agent_qa_explain_brief` and `agent_qa_show_knowledge_record` are the drill-down, and the tool carries a 100000-character result-size hint so a host keeps the one-shot answer whole."
1108
+ }),
1109
+ Object.freeze({
1110
+ opId: "runs.evidence",
1111
+ ruledBy: "423",
1112
+ ruling: "D-04 (dated 2026-09-15, 423-21): not a mid-flight workflow read per D-02 \u2014 this is one COMPLETED run step's parsed evidence document (console, network, variables, accessibility or ARIA), read by the dashboard evidence tabs, and it is unprojected on exactly the footing its sibling `runs.detail` is. A network document alone may carry hundreds of records, so it is a large one-shot payload with no shape a `tools/call` round trip wants, and a planner mid-flight has no use for a finished step's captured traffic. The raw SHA bytes stay on `runs.artifact` and the two HTTP doors. Deferred as its own seam, not omitted.",
1113
+ addendum: "D-07 (dated 2026-09-28, 431-04): projected as the `runs.evidence` arm of `agent_qa_get_run_evidence`. A caller asks for ONE step document by the exact address `agent_qa_get_run_overview` supplies, so the answer is one addressed document rather than a run's captured traffic. Masking is unchanged: the op stays on the cold read-scope allowlist. The tool carries the 100000-character result-size hint."
1114
+ }),
1115
+ Object.freeze({
1116
+ opId: "runs.artifact",
1117
+ ruledBy: "405",
1118
+ ruling: "D-12/D-13 (dated 2026-08-27, 405-05): not a mid-flight workflow read per D-02 \u2014 this resolves ONE completed run's content-addressed evidence blob, and its result is a base64 body. A `tools/call` round trip is the wrong carriage for bytes: a conversation cannot render a PNG, and a tool that returned one would spend a context window on a payload no reader can read. The two HTTP doors are its carriage, and 408's embed is the surface that renders it. Deferred as its own seam, not omitted. Phase 424 (dated 2026-09-20): the recording fact rides runs.detail; runs.artifact answers a bounded slice when range is supplied; pulling a whole recording through unary base64 is permitted and not recommended; no recording tool is projected because the MX ceiling of 22 is fully allocated.",
1119
+ addendum: "D-07 (dated 2026-09-28, 431-04): projected as the `runs.artifact` arm of `agent_qa_get_run_evidence`. The blob is addressed by the sha the overview supplies, with no step-to-sha lookup inside the tool, and the rendering objection is answered by the image result arm 431-05 adds. The Phase 424 note that the MX ceiling of 22 was fully allocated is superseded: the ceiling is 24 (D-01)."
1120
+ })
1121
+ ]);
1122
+
1123
+ // src/masked-result.ts
1124
+ var import_agent_qa_schema3 = require("@vostride/agent-qa-schema");
1125
+ var WITHHELD_TEXT = "This result could not be returned: part of its output could not be masked in place \u2014 a value whose hidden form would not be the same kind of value, or two names that mask to one name \u2014 so the unmasked object was withheld rather than served.";
1126
+ var UnmaskableValueError = class extends Error {
1127
+ };
1128
+ function maskJsonLeaves(value, maskText) {
1129
+ if (typeof value === "string") return maskText(value);
1130
+ if (value === null) return null;
1131
+ if (Array.isArray(value)) {
1132
+ return value.map((entry) => maskJsonLeaves(entry, maskText));
1133
+ }
1134
+ if (typeof value === "object") {
1135
+ const masked = {};
1136
+ for (const [key, entry] of Object.entries(value)) {
1137
+ const maskedKey = maskText(key);
1138
+ if (Object.hasOwn(masked, maskedKey)) {
1139
+ throw new UnmaskableValueError("two keys of one object mask to the same name");
1140
+ }
1141
+ Object.defineProperty(masked, maskedKey, {
1142
+ value: maskJsonLeaves(entry, maskText),
1143
+ enumerable: true,
1144
+ writable: true,
1145
+ configurable: true
1146
+ });
1147
+ }
1148
+ return masked;
1149
+ }
1150
+ const spelling = JSON.stringify(value) ?? "null";
1151
+ if (maskText(spelling) !== spelling) {
1152
+ throw new UnmaskableValueError("a non-string leaf spells a registered value");
1153
+ }
1154
+ return value;
1155
+ }
1156
+ function maskJsonDocument(output, maskText) {
1157
+ const serialized = JSON.stringify(output) ?? "null";
1158
+ let parsed;
1159
+ try {
1160
+ parsed = JSON.parse(serialized);
1161
+ } catch {
1162
+ return void 0;
1163
+ }
1164
+ let value;
222
1165
  try {
223
- return JSON.parse(firstText.text);
1166
+ value = maskJsonLeaves(parsed, maskText);
224
1167
  } catch {
225
1168
  return void 0;
226
1169
  }
1170
+ const text = JSON.stringify(value);
1171
+ if (text === void 0) return void 0;
1172
+ return { value, text };
227
1173
  }
228
- function categorizeMcpError(errOrResult) {
229
- if (errOrResult instanceof import_zod.z.ZodError) return "validation";
230
- const parsed = parseToolJsonContent(errOrResult);
231
- const candidate = parsed && typeof parsed === "object" ? parsed : void 0;
232
- if (candidate && candidate.ok === false) return "dashboard_error";
233
- if (errOrResult instanceof Error) return "tool_error";
234
- if (isErrorResult(errOrResult)) return "tool_error";
235
- return "unknown";
1174
+ function maskedToolResult(output, maskText) {
1175
+ const masked = maskJsonDocument(output, maskText);
1176
+ if (masked === void 0) {
1177
+ return {
1178
+ content: [{ type: "text", text: WITHHELD_TEXT }],
1179
+ isError: true
1180
+ };
1181
+ }
1182
+ return {
1183
+ content: [{ type: "text", text: masked.text }],
1184
+ structuredContent: masked.value
1185
+ };
236
1186
  }
237
- function extractExplicitCanonicalRunId(input) {
238
- if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
239
- const runId = input.runId;
240
- return typeof runId === "string" && (0, import_agent_qa_ids2.isCanonicalId)("run", runId) ? runId : void 0;
1187
+ function maskedToolError(err, maskText) {
1188
+ const message = err instanceof Error ? err.message : String(err);
1189
+ return {
1190
+ content: [
1191
+ {
1192
+ type: "text",
1193
+ text: maskText(message)
1194
+ }
1195
+ ],
1196
+ isError: true
1197
+ };
241
1198
  }
242
- function extractSuccessfulEnqueueRunId(result) {
243
- if (isErrorResult(result)) return void 0;
244
- const parsed = parseToolJsonContent(result);
245
- const container = parsed && typeof parsed === "object" ? parsed : void 0;
246
- if (!container) return void 0;
247
- const body = container.body && typeof container.body === "object" ? container.body : container;
248
- for (const key of ["runId", "id", "parentRunId"]) {
249
- const value = body[key];
250
- if (typeof value === "string" && (0, import_agent_qa_ids2.isCanonicalId)("run", value)) return value;
1199
+ var RUN_EVIDENCE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg"]);
1200
+ var RUN_EVIDENCE_IMAGE_TEXT_MARKER = "[image attached as image content]";
1201
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
1202
+ var PNG_IEND_TRAILER = [
1203
+ 0,
1204
+ 0,
1205
+ 0,
1206
+ 0,
1207
+ 73,
1208
+ 69,
1209
+ 78,
1210
+ 68,
1211
+ 174,
1212
+ 66,
1213
+ 96,
1214
+ 130
1215
+ ];
1216
+ var JPEG_START = [255, 216, 255];
1217
+ var JPEG_END = [255, 217];
1218
+ var CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
1219
+ function refusalText(refusal, contentType) {
1220
+ switch (refusal) {
1221
+ case "artifact-range-fragment":
1222
+ return "artifact-range-fragment: agent_qa_get_run_evidence returns a screenshot only as one complete PNG or JPEG image, and this range covers part of one. Call runs.artifact without a range.";
1223
+ case "artifact-not-image":
1224
+ return `artifact-not-image: this artifact is ${contentType ?? "not typed"}, not a PNG or JPEG screenshot, and agent_qa_get_run_evidence relays no other artifact bytes. Read a step document through action runs.evidence.`;
1225
+ case "artifact-image-invalid":
1226
+ return "artifact-image-invalid: the artifact is typed as an image, but its bytes are not one complete PNG or JPEG, so they were not returned.";
1227
+ case "artifact-image-withheld":
1228
+ return "artifact-image-withheld: masking changed the encoded screenshot, so neither the image nor its bytes were returned.";
251
1229
  }
252
- return void 0;
253
1230
  }
254
- function resolveDashboardApiUrl(dashboardUrl, apiPath) {
255
- const base = dashboardUrl.endsWith("/") ? dashboardUrl : `${dashboardUrl}/`;
256
- const normalizedPath = apiPath.startsWith("/") ? apiPath.slice(1) : apiPath;
257
- return new URL(normalizedPath, base).toString();
1231
+ function hasBytesAt(bytes, expected, offset) {
1232
+ if (offset < 0 || bytes.length < offset + expected.length) return false;
1233
+ return expected.every((byte, index) => bytes[offset + index] === byte);
258
1234
  }
259
- function withQuery(path, query) {
260
- const params = new URLSearchParams();
261
- for (const [key, value] of Object.entries(query)) {
262
- if (value !== void 0) params.set(key, String(value));
1235
+ function isCompleteImage(contentType, bytes) {
1236
+ if (contentType === "image/png") {
1237
+ return hasBytesAt(bytes, PNG_SIGNATURE, 0) && hasBytesAt(bytes, PNG_IEND_TRAILER, bytes.length - PNG_IEND_TRAILER.length);
263
1238
  }
264
- const queryString = params.toString();
265
- return queryString ? `${path}?${queryString}` : path;
1239
+ return hasBytesAt(bytes, JPEG_START, 0) && hasBytesAt(bytes, JPEG_END, bytes.length - JPEG_END.length);
266
1240
  }
267
- async function callDashboardApi(input) {
268
- const response = await fetch(resolveDashboardApiUrl(input.dashboardUrl, input.path), {
269
- method: input.method ?? "GET",
270
- headers: input.body === void 0 ? void 0 : { "content-type": "application/json" },
271
- body: input.body === void 0 ? void 0 : JSON.stringify(input.body)
1241
+ function decodeCanonicalBase64(text) {
1242
+ if (!CANONICAL_BASE64.test(text)) return void 0;
1243
+ let binary;
1244
+ try {
1245
+ binary = atob(text);
1246
+ } catch {
1247
+ return void 0;
1248
+ }
1249
+ if (btoa(binary) !== text) return void 0;
1250
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
1251
+ }
1252
+ function coversWholeBlob(artifact) {
1253
+ if (artifact.range === void 0) {
1254
+ return artifact.totalLength === void 0 || artifact.totalLength === artifact.byteLength;
1255
+ }
1256
+ return artifact.totalLength !== void 0 && artifact.range.start === 0 && artifact.range.end === artifact.totalLength - 1 && artifact.byteLength === artifact.totalLength;
1257
+ }
1258
+ function decorateRunEvidenceImage(input) {
1259
+ const { result } = input;
1260
+ if (result.isError === true) return result;
1261
+ const refuse = (refusal, contentType) => ({
1262
+ content: [{ type: "text", text: input.maskText(refusalText(refusal, contentType)) }],
1263
+ isError: true,
1264
+ ...result._meta === void 0 ? {} : { _meta: result._meta }
272
1265
  });
273
- const text = await response.text();
274
- let body = null;
275
- if (text.length > 0) {
276
- try {
277
- body = JSON.parse(text);
278
- } catch {
279
- body = text;
280
- }
1266
+ const parsed = import_agent_qa_schema3.RunsArtifactResultSchema.safeParse(input.output);
1267
+ if (!parsed.success) return refuse("artifact-image-invalid");
1268
+ const artifact = parsed.data;
1269
+ if (!RUN_EVIDENCE_IMAGE_TYPES.has(artifact.contentType)) {
1270
+ return refuse("artifact-not-image", artifact.contentType);
1271
+ }
1272
+ if (!coversWholeBlob(artifact)) return refuse("artifact-range-fragment");
1273
+ const masked = result.structuredContent;
1274
+ if (masked?.bytesBase64 !== artifact.bytesBase64 || masked.contentType !== artifact.contentType) {
1275
+ return refuse("artifact-image-withheld");
281
1276
  }
1277
+ const bytes = decodeCanonicalBase64(artifact.bytesBase64);
1278
+ if (bytes === void 0 || bytes.length !== artifact.byteLength || !isCompleteImage(artifact.contentType, bytes)) {
1279
+ return refuse("artifact-image-invalid");
1280
+ }
1281
+ const text = JSON.stringify({ ...masked, bytesBase64: RUN_EVIDENCE_IMAGE_TEXT_MARKER });
282
1282
  return {
283
- ok: response.ok,
284
- status: response.status,
285
- body
1283
+ ...result,
1284
+ content: [
1285
+ ...result.content.map((block) => block.type === "text" ? { type: "text", text } : block),
1286
+ { type: "image", data: artifact.bytesBase64, mimeType: artifact.contentType }
1287
+ ]
286
1288
  };
287
1289
  }
288
- function collectStrings(value, output = [], limit = 80) {
289
- if (output.length >= limit) return output;
290
- if (typeof value === "string" && value.trim().length > 0) {
291
- output.push(value);
292
- } else if (Array.isArray(value)) {
293
- for (const item of value) collectStrings(item, output, limit);
294
- } else if (value && typeof value === "object") {
295
- for (const child of Object.values(value)) collectStrings(child, output, limit);
296
- }
297
- return output;
1290
+
1291
+ // src/migration-map.ts
1292
+ var V1_TOOL_MIGRATION_MAP = Object.freeze([
1293
+ // 1
1294
+ Object.freeze({
1295
+ v1Name: "agent_qa_discover",
1296
+ arm: "retired",
1297
+ note: "not-a-gap (397, 2026-08-16): the protocol answers discovery itself, so a hand-written catalogue tool beside it would be a second, drifting answer to one question.",
1298
+ route: "`tools/list` (plus SEP-2575 `server/discover`) answers machine discovery; 398's build-time capability catalog, carried by `agent-qa init --agent`, answers the human-facing version."
1299
+ }),
1300
+ // 2
1301
+ Object.freeze({
1302
+ v1Name: "agent_qa_get_config",
1303
+ arm: "retired",
1304
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): the inventory rules this row `n/a -> projected by 396`, but `config.get` is in UNPROJECTED_OPS after 396 plan 04 \u2014 not a mid-flight workflow read per D-02 \u2014 so there is no live tool to rename to.",
1305
+ route: "no MCP tool reads configuration; the workspace posture an agent needs reaches it through `agent-qa init --agent`'s manifest (398 D-16), by env-ref name and never by value."
1306
+ }),
1307
+ // 3
1308
+ Object.freeze({
1309
+ v1Name: "agent_qa_schema_reference",
1310
+ arm: "retired",
1311
+ note: "not-a-gap (397, 2026-08-16): a served COPY of a contract is exactly the drift that advertising the live contract replaced.",
1312
+ route: "every tool advertises its op's live `inputSchema`/`outputSchema` in `tools/list` (396 D-16), so a caller reads the contract the gate actually validates with; a fold lists its arms' fields as one object, and its `action` field says which fields each action takes."
1313
+ }),
1314
+ // 4
1315
+ Object.freeze({
1316
+ v1Name: "agent_qa_validate_definition",
1317
+ arm: "retired",
1318
+ note: "not-a-gap (397, 2026-08-16): a standalone linter over arbitrary text is a second validator that can disagree with the one that gates writes.",
1319
+ route: "validation reaches a caller as `authoring.draft.verify`'s verdict inside the authoring funnel \u2014 the verify arm of `agent_qa_refine_test`."
1320
+ }),
1321
+ // 5
1322
+ Object.freeze({
1323
+ v1Name: "agent_qa_generate_id",
1324
+ arm: "retired",
1325
+ note: "not-a-gap (397, 2026-08-16): died with the CRUD model. Minting a canonical id on a surface is what 394-11 removed from the CLI, for this same reason.",
1326
+ route: "ids are minted host-side where the entity is written; a caller names what it wants and reads the id back off the entity card `agent_qa_get_entity` returns."
1327
+ }),
1328
+ // 6
1329
+ Object.freeze({
1330
+ v1Name: "agent_qa_validate_id",
1331
+ arm: "retired",
1332
+ note: "not-a-gap (397, 2026-08-16): died with the CRUD model. A standalone validator would be a second answer to a question already answered at every parse point.",
1333
+ route: "the one URI codec (`packages/schema/src/entity/uri.ts`) validates every `agentqa://` reference as it is parsed, including the one `agent_qa_get_entity` resolves."
1334
+ }),
1335
+ // 7
1336
+ Object.freeze({
1337
+ v1Name: "agent_qa_list_tests",
1338
+ arm: "pending",
1339
+ note: "gap (397, 2026-08-16): nothing in the registry enumerates the test corpus \u2014 `entities.resolve` resolves a reference to an entity, which is a different question.",
1340
+ route: "until the owning milestone mints `entities.list`, a known reference resolves through `agent_qa_get_entity`; enumeration has no tool.",
1341
+ owner: "M5 (live view & run invocation)"
1342
+ }),
1343
+ // 8
1344
+ Object.freeze({
1345
+ v1Name: "agent_qa_read_test",
1346
+ arm: "renamed-to",
1347
+ v2Tool: "agent_qa_get_entity",
1348
+ note: "projected by 396 (inventory row 8, 2026-08-16): `entities.get` is D-02's universal entity read, kind TST, behind the one read door."
1349
+ }),
1350
+ // 9
1351
+ Object.freeze({
1352
+ v1Name: "agent_qa_validate_test",
1353
+ arm: "retired",
1354
+ note: "not-a-gap (397, 2026-08-16): row 4's ruling. `authoring.draft.verify` is the only validator, and it validates a draft in the funnel rather than arbitrary text a caller pasted.",
1355
+ route: "the verify arm of `agent_qa_refine_test` returns the same verdict that gates the write."
1356
+ }),
1357
+ // 10
1358
+ Object.freeze({
1359
+ v1Name: "agent_qa_create_test",
1360
+ arm: "renamed-to",
1361
+ v2Tool: "agent_qa_write_test",
1362
+ note: "projected by 396 (inventory row 10, 2026-08-16): `authoring.draft.write` is workflow-coarse and verifies a draft rather than performing v1's blind write, so the input shape differs. Since 2026-10-02 (432-40, D-06) it writes the test file in the call unless a keep-gate rejected the draft."
1363
+ }),
1364
+ // 11
1365
+ Object.freeze({
1366
+ v1Name: "agent_qa_update_test",
1367
+ arm: "renamed-to",
1368
+ v2Tool: "agent_qa_refine_test",
1369
+ note: "projected by 396 (inventory row 11, 2026-08-16): `authoring.draft.edit` is the edit arm of the refine fold, selected by the fold discriminant."
1370
+ }),
1371
+ // 12
1372
+ Object.freeze({
1373
+ v1Name: "agent_qa_delete_test",
1374
+ arm: "retired",
1375
+ note: "not-a-gap (397, 2026-08-16): a rule, not an absence. MCPS-02 forbids a surface delete verb for irreversible deletes.",
1376
+ route: "removing a committed test is a repository edit a person makes and reviews; no surface has a delete door and none is coming."
1377
+ }),
1378
+ // 13
1379
+ Object.freeze({
1380
+ v1Name: "agent_qa_list_suites",
1381
+ arm: "pending",
1382
+ note: `gap (397, 2026-08-16): folded into row 7's verb \u2014 enumerating the corpus answers "which tests exist, and in which files". A suite is a file, so this is not a second registry.`,
1383
+ route: "until `entities.list` lands, suite membership is read from the workspace files themselves; the TST records inside them resolve through `agent_qa_get_entity`.",
1384
+ owner: "M5 (live view & run invocation)"
1385
+ }),
1386
+ // 14
1387
+ Object.freeze({
1388
+ v1Name: "agent_qa_read_suite",
1389
+ arm: "retired",
1390
+ note: "not-a-gap (397, 2026-08-16): there is no suite RECORD to read. `ENTITY_KINDS` has no suite kind and a suite is a committed file addressed by workspace path.",
1391
+ route: "the TST records inside a suite are `agent_qa_get_entity`'s job; the file itself is read from the workspace."
1392
+ }),
1393
+ // 15
1394
+ Object.freeze({
1395
+ v1Name: "agent_qa_validate_suite",
1396
+ arm: "retired",
1397
+ note: "not-a-gap (397, 2026-08-16): row 4's ruling \u2014 one validator, inside the funnel.",
1398
+ route: "the verify arm of `agent_qa_refine_test` validates the draft that joins the suite file."
1399
+ }),
1400
+ // 16
1401
+ Object.freeze({
1402
+ v1Name: "agent_qa_create_suite",
1403
+ arm: "retired",
1404
+ note: "not-a-gap (397, 2026-08-16): a suite is a committed file the authoring funnel writes. A blind create is the v1 write the verified authoring funnel replaced (re-spelled 2026-10-02, 432-40, D-06: the funnel writes in the call).",
1405
+ route: "`agent_qa_write_test` names the suite file a draft joins and writes it in the call, unless a keep-gate rejected the draft."
1406
+ }),
1407
+ // 17
1408
+ Object.freeze({
1409
+ v1Name: "agent_qa_update_suite",
1410
+ arm: "retired",
1411
+ note: "not-a-gap (397, 2026-08-16): row 16's ruling, through the edit arm instead of the write (re-spelled 2026-10-02, 432-40, D-06: the edit lands in the call).",
1412
+ route: "`agent_qa_refine_test` edits the written test; the suite file changes in the call."
1413
+ }),
1414
+ // 18
1415
+ Object.freeze({
1416
+ v1Name: "agent_qa_delete_suite",
1417
+ arm: "retired",
1418
+ note: "not-a-gap (397, 2026-08-16): row 12's MCPS-02 rule, applied to the file rather than the record.",
1419
+ route: "removing a suite file is a repository edit a person makes and reviews; no surface door exists."
1420
+ }),
1421
+ // 19
1422
+ Object.freeze({
1423
+ v1Name: "agent_qa_list_hooks",
1424
+ arm: "pending",
1425
+ note: "gap (397, 2026-08-16, corrected the same day): the 396 reason overstated `config.get`, which projects the resolved CONFIG's leaves; hook definitions live in the separate `workspace.hooksFile`, so no registered op reads them today.",
1426
+ route: "until the owning milestone re-opens the hook execution model, hooks are read from `workspace.hooksFile` in the workspace; no tool enumerates them.",
1427
+ owner: "M7 (skills layer)"
1428
+ }),
1429
+ // 20
1430
+ Object.freeze({
1431
+ v1Name: "agent_qa_read_hook",
1432
+ arm: "pending",
1433
+ note: "gap (397, 2026-08-16): with row 19, ONE hook-read verb and not two. What `config.get` shows is `workspace.hooksFile` \u2014 the path, not the hooks.",
1434
+ route: "until that verb lands, a single hook is read from the hooks file itself; no tool projects it.",
1435
+ owner: "M7 (skills layer)"
1436
+ }),
1437
+ // 21
1438
+ Object.freeze({
1439
+ v1Name: "agent_qa_create_hook",
1440
+ arm: "renamed-to",
1441
+ v2Tool: "agent_qa_change_config",
1442
+ note: "projected by 396 (inventory row 21, 2026-08-16): `config.hook.add` is an arm of the config fold. Since 2026-09-30 (432-12, D-05) the fold is `agent_qa_change_config` and the hook is written in the call, with the files it changed on the answer."
1443
+ }),
1444
+ // 22
1445
+ Object.freeze({
1446
+ v1Name: "agent_qa_update_hook",
1447
+ arm: "pending",
1448
+ note: "gap (397, 2026-08-16): `config.field.set` reaches any schema-addressable CONFIG path and the hooks file is not one, so an existing hook has no edit verb. Minting one now would pin an edit shape to a contract the owning milestone revisits.",
1449
+ route: "adding a hook goes through `agent_qa_change_config`; editing an existing one is a hooks-file edit a person makes until the owner rules on the verb.",
1450
+ owner: "M7 (skills layer)"
1451
+ }),
1452
+ // 23
1453
+ Object.freeze({
1454
+ v1Name: "agent_qa_delete_hook",
1455
+ arm: "retired",
1456
+ note: "not-a-gap (397, 2026-08-16): row 12's MCPS-02 rule.",
1457
+ route: "removing a hook is an edit to the hooks file that a person makes and reviews."
1458
+ }),
1459
+ // 24
1460
+ Object.freeze({
1461
+ v1Name: "agent_qa_run_hook",
1462
+ arm: "retired",
1463
+ note: `not-a-gap (397, 2026-08-16): a hook's whole contract is "runs at this lifecycle point". Invoking one by name from a surface is arbitrary script execution decoupled from the lifecycle that gives it meaning \u2014 the private-power shape M4 abolished.`,
1464
+ route: "hooks run at their declared lifecycle points during a run started by `agent_qa_start_run`; nothing invokes one out of band."
1465
+ }),
1466
+ // 25
1467
+ Object.freeze({
1468
+ v1Name: "agent_qa_enqueue_test_run",
1469
+ arm: "renamed-to",
1470
+ v2Tool: "agent_qa_start_run",
1471
+ note: "projected by 396 (inventory row 25, 2026-08-16): `runs.start` is workflow-coarse, and the test scope is an argument rather than a tool of its own."
1472
+ }),
1473
+ // 26
1474
+ Object.freeze({
1475
+ v1Name: "agent_qa_enqueue_suite_run",
1476
+ arm: "renamed-to",
1477
+ v2Tool: "agent_qa_start_run",
1478
+ note: "projected by 396 (inventory row 26, 2026-08-16): the SAME op as row 25 at suite scope, which is why D-01 allows one tool over both."
1479
+ }),
1480
+ // 27
1481
+ Object.freeze({
1482
+ v1Name: "agent_qa_get_run",
1483
+ arm: "retired",
1484
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): the inventory rules this row `n/a -> projected by 396`, but `runs.get` is in UNPROJECTED_OPS after 396 plan 04 \u2014 not a mid-flight workflow read per D-02 \u2014 so there is no live tool to rename to.",
1485
+ route: "the entity door renders a run's card: `agent_qa_get_entity` on the run reference. `agent_qa_list_runs` enumerates recent runs."
1486
+ }),
1487
+ // 28
1488
+ Object.freeze({
1489
+ v1Name: "agent_qa_get_run_steps",
1490
+ arm: "retired",
1491
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): ruled `n/a -> projected by 396` against `runs.get`/`runs.events`, both of which are in UNPROJECTED_OPS \u2014 `runs.events` is a stream op besides, and a `tools/call` is one round trip with no shape for a resumable feed.",
1492
+ route: "`agent_qa_get_run_digest` carries what triage needs from a run, including its steps' outcome; the run's card comes from `agent_qa_get_entity`."
1493
+ }),
1494
+ // 29
1495
+ Object.freeze({
1496
+ v1Name: "agent_qa_get_run_logs",
1497
+ arm: "retired",
1498
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): ruled `n/a -> projected by 396` against `runs.events`, which is in UNPROJECTED_OPS as a stream op with no single-round-trip shape.",
1499
+ route: "`agent_qa_get_run_digest` carries the triage-relevant remainder of a run; the full event feed is read from the run artifacts on disk, not over this surface."
1500
+ }),
1501
+ // 30
1502
+ Object.freeze({
1503
+ v1Name: "agent_qa_get_run_execution_logs",
1504
+ arm: "retired",
1505
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): row 29's reading \u2014 `runs.events` is unprojected, so the second logs alias has no live tool either.",
1506
+ route: "`agent_qa_get_run_digest` for triage; execution logs live in the run's artifacts on disk."
1507
+ }),
1508
+ // 31
1509
+ Object.freeze({
1510
+ v1Name: "agent_qa_get_run_artifact",
1511
+ arm: "pending",
1512
+ note: "gap (397, 2026-08-16): artifact bytes over a tool result is a disclosure surface nobody has scoped, so the read is routed WITH that scoping rather than minted without it \u2014 an artifact read without it is the gap made worse.",
1513
+ route: "until the owning milestone ships artifact resolution, artifacts are read from the run directory on disk; `agent_qa_get_run_digest` names what a run produced.",
1514
+ owner: "M5 (artifact resolution route)"
1515
+ }),
1516
+ // 32
1517
+ Object.freeze({
1518
+ v1Name: "agent_qa_cancel_run",
1519
+ arm: "pending",
1520
+ note: "gap (397, 2026-08-16): ruled a REAL gap as `runs.cancel` and routed to its owner rather than minted in 397, because D-17 binds the adjudicator too.",
1521
+ route: "until `runs.cancel` lands, a run is stopped by the host that started it; this surface starts runs through `agent_qa_start_run` and cannot stop one.",
1522
+ owner: "M5 (live-view control arbiter)"
1523
+ }),
1524
+ // 33
1525
+ Object.freeze({
1526
+ v1Name: "agent_qa_classify_failure",
1527
+ arm: "retired",
1528
+ note: "not-a-gap (397, 2026-08-16): classification on demand over dashboard-fetched data is the private data path M4 abolished, not a capability that went missing.",
1529
+ route: "`analysis.get` reads the verdict the `analyze-run` job produced; over this surface the triage answer is `agent_qa_get_run_digest`."
1530
+ })
1531
+ ]);
1532
+
1533
+ // src/projection.ts
1534
+ var JSON_SCHEMA_CONVERSION_TARGET = "draft-2020-12";
1535
+ function isReadOnly(specs) {
1536
+ return specs.every((spec) => spec.risk.readOnly === true);
298
1537
  }
299
- function classifyByNeedle(haystack, evidence, needles, category, confidence) {
300
- const match = needles.find((needle) => haystack.includes(needle));
301
- if (!match) return null;
302
- const matchingEvidence = evidence.find((item) => item.toLowerCase().includes(match)) ?? evidence[0] ?? match;
1538
+ function deriveAnnotations(specs) {
1539
+ const readOnlyHint = isReadOnly(specs);
303
1540
  return {
304
- category,
305
- confidence,
306
- evidence: [matchingEvidence].filter(Boolean).slice(0, 5),
307
- recentRelatedCount: 0
1541
+ readOnlyHint,
1542
+ destructiveHint: specs.some((spec) => spec.risk.destructive === true),
1543
+ idempotentHint: readOnlyHint
308
1544
  };
309
1545
  }
310
- function classifyRunFailureFromDashboardData(input) {
311
- const run = input.runDetail && typeof input.runDetail === "object" ? input.runDetail.run : void 0;
312
- const status = run && typeof run === "object" ? run.status : void 0;
313
- const evidence = collectStrings([input.runDetail, input.artifact, input.logs, input.executionLogs]);
314
- const haystack = evidence.join("\n").toLowerCase();
315
- const recentRelatedCount = Array.isArray(input.recentRuns?.runs) ? input.recentRuns.runs.length : 0;
316
- if (status && status !== "failed") {
317
- return {
318
- category: "passed",
319
- confidence: 0.95,
320
- evidence: [`Run status is ${String(status)}.`],
321
- recentRelatedCount
322
- };
1546
+ function describe(projection, specs) {
1547
+ const composition = compositionOf(projection);
1548
+ if (composition === "single") return specs[0].summary;
1549
+ const arms = specs.map((spec) => `${spec.id}: ${spec.summary}`).join("\n");
1550
+ if (composition === "pipeline") {
1551
+ return `${arms}
1552
+ These run in the order listed; the second runs only when the first resolves.`;
1553
+ }
1554
+ return `${arms}
1555
+ Select one with "${projection.discriminant}", whose value is the op id above.`;
1556
+ }
1557
+ function degradationSentence(degradation) {
1558
+ return `
1559
+ This tool advertises no outputSchema: its result contract (${degradation.schema}) contains a construct JSON Schema cannot represent, and a narrowed stand-in would describe a shape nothing produces. The result still arrives as structuredContent.`;
1560
+ }
1561
+ function probeOutputSchema(projection, output) {
1562
+ const declared = DEGRADED_OUTPUT_CONTRACTS.find((entry) => entry.tool === projection.name);
1563
+ let failure;
1564
+ try {
1565
+ output["~standard"].jsonSchema.output({ target: JSON_SCHEMA_CONVERSION_TARGET });
1566
+ } catch (err) {
1567
+ failure = err instanceof Error ? err.message : String(err);
1568
+ }
1569
+ if (failure === void 0) {
1570
+ if (declared === void 0) return output;
1571
+ throw new McpProjectionError(
1572
+ projection.name,
1573
+ `is listed in DEGRADED_OUTPUT_CONTRACTS for ${declared.schema}, but that contract now converts cleanly in output mode; remove the entry so the schema is advertised again`
1574
+ );
1575
+ }
1576
+ if (declared === void 0) {
1577
+ throw new McpProjectionError(
1578
+ projection.name,
1579
+ `advertises an output contract JSON Schema cannot represent (${failure}). Declare it in DEGRADED_OUTPUT_CONTRACTS with the measured message, or give the op a contract that converts \u2014 an unrepresentable outputSchema throws inside tools/list and takes the whole listing down for every caller`
1580
+ );
1581
+ }
1582
+ return void 0;
1583
+ }
1584
+ function toRegisterToolConfig(projection, specs, advertised, maskText) {
1585
+ if (specs.length === 0) {
1586
+ throw new McpProjectionError(
1587
+ projection.name,
1588
+ "projects no ops; a tool with no arm has nothing to validate against and nothing to call"
1589
+ );
323
1590
  }
324
- const checks = [
325
- [["timed out", "timeout"], "timeout", 0.9],
326
- [["appium server failed", "appium"], "appium_startup", 0.9],
327
- [["browser closed", "target page, context or browser has been closed", "browser disconnect"], "browser_disconnect", 0.85],
328
- [["hook_not_runnable", "hook failed", "hook registry", "hook"], "hook_failure", 0.8],
329
- [["not found", "no element", "strict mode violation", "selector"], "element_not_found", 0.8],
330
- [["assert", "expected", "verify"], "assertion_failure", 0.75],
331
- [["econnrefused", "enotfound", "network", "docker"], "infrastructure", 0.7]
332
- ];
333
- for (const [needles, category, confidence] of checks) {
334
- const result = classifyByNeedle(haystack, evidence, needles, category, confidence);
335
- if (result) return { ...result, recentRelatedCount };
1591
+ if (specs.length !== projection.ops.length) {
1592
+ throw new McpProjectionError(
1593
+ projection.name,
1594
+ `declares ${projection.ops.length} ops but was derived from ${specs.length}`
1595
+ );
336
1596
  }
1597
+ const outputSchema = probeOutputSchema(projection, advertised.output);
1598
+ const degradation = outputSchema === void 0 ? DEGRADED_OUTPUT_CONTRACTS.find((entry) => entry.tool === projection.name) : void 0;
1599
+ const description = describe(projection, specs);
1600
+ const meta = listingMeta(projection);
1601
+ const fold = compositionOf(projection) === "union";
1602
+ const inputSchema = fold ? flatFoldInput(projection, advertised.input) : advertised.input;
1603
+ if (fold) inputSchema["~standard"].jsonSchema.input({ target: JSON_SCHEMA_CONVERSION_TARGET });
337
1604
  return {
338
- category: "unknown_failure",
339
- confidence: 0.3,
340
- evidence: evidence.slice(0, 5),
341
- recentRelatedCount
1605
+ title: projection.name,
1606
+ description: maskText(
1607
+ degradation === void 0 ? description : description + degradationSentence(degradation)
1608
+ ),
1609
+ inputSchema,
1610
+ ...outputSchema === void 0 ? {} : { outputSchema },
1611
+ annotations: deriveAnnotations(specs),
1612
+ // 408 D-05: the App reference, DERIVED from the projection's own carrier declaration by the
1613
+ // same pipeline that derives the annotations above — never a pair of tool names written here.
1614
+ // The conditional spread is the house absent-field rule: a non-carrier's config has no `_meta`
1615
+ // key at all.
1616
+ //
1617
+ // NOT MASKED, and it joins the two exemptions the header names for their reason rather than by
1618
+ // analogy. `resourceUri` is the address constant this package publishes — a declared literal in
1619
+ // this repository's own source, exactly like the `title` above it — and a masker that rewrote
1620
+ // it would point every host at an address the resource codec refuses, which is the tool-rename
1621
+ // failure with a URI scheme attached. Nothing interpolated into it comes from a caller, from
1622
+ // storage or from a resolved value; the run id never appears here, because this is the
1623
+ // TEMPLATE and a per-call reference is composed downstream. The result-size hint (431 D-05) is
1624
+ // a declared constant too, and rides the same record (`listingMeta`).
1625
+ ...meta === void 0 ? {} : { _meta: meta }
1626
+ };
1627
+ }
1628
+ function listingMeta(projection) {
1629
+ const meta = {
1630
+ ...projection.ui === void 0 ? {} : { ui: { resourceUri: APP_RESOURCE_URI_TEMPLATE } },
1631
+ ...projection.maxResultSizeChars === void 0 ? {} : { [MAX_RESULT_SIZE_META_KEY]: projection.maxResultSizeChars }
342
1632
  };
1633
+ return Object.keys(meta).length === 0 ? void 0 : meta;
343
1634
  }
344
- function maskSensitive(value, parentKey = "") {
345
- const authRedacted = (0, import_agent_qa_core2.redactAuthStateValue)(value);
346
- const lower = parentKey.toLowerCase();
347
- if (typeof authRedacted === "string" && authRedacted !== value) {
348
- return authRedacted;
1635
+ function flatFoldInput(projection, input) {
1636
+ const standard = input["~standard"];
1637
+ return {
1638
+ "~standard": {
1639
+ version: standard.version,
1640
+ vendor: standard.vendor,
1641
+ validate: (value, options) => standard.validate(value, options),
1642
+ jsonSchema: {
1643
+ input: (options) => flatFoldDocument(projection, standard.jsonSchema.input(options)),
1644
+ output: (options) => standard.jsonSchema.output(options)
1645
+ }
1646
+ }
1647
+ };
1648
+ }
1649
+ function flatFoldDocument(projection, document) {
1650
+ const key = projection.discriminant;
1651
+ const { oneOf, type: _type, ...rest } = document;
1652
+ const arms = Array.isArray(oneOf) ? oneOf : [];
1653
+ const unsupported = ["anyOf", "allOf", "properties"].filter((name) => rest[name] !== void 0);
1654
+ if (arms.length !== projection.ops.length || unsupported.length > 0) {
1655
+ throw new McpProjectionError(
1656
+ projection.name,
1657
+ `has an input contract that is not a ${projection.ops.length}-arm union of objects, so it cannot be listed as one object`
1658
+ );
349
1659
  }
350
- if (typeof authRedacted === "string" && (lower.includes("key") || lower.includes("token") || lower.includes("secret") || lower.includes("password"))) {
351
- return authRedacted.length > 8 ? `${authRedacted.slice(0, 3)}****${authRedacted.slice(-4)}` : "****";
1660
+ const ops = [];
1661
+ const fields = /* @__PURE__ */ new Map();
1662
+ let actionNote;
1663
+ let strict = true;
1664
+ for (const arm of arms) {
1665
+ const properties2 = arm.properties ?? {};
1666
+ const opId = properties2[key]?.const;
1667
+ if (arm.type !== "object" || typeof opId !== "string" || !projection.ops.includes(opId)) {
1668
+ throw new McpProjectionError(
1669
+ projection.name,
1670
+ `has a union arm that is not an object selected by a declared ${key}`
1671
+ );
1672
+ }
1673
+ ops.push(opId);
1674
+ if (arm.additionalProperties !== false) strict = false;
1675
+ actionNote ??= properties2[key]?.description;
1676
+ const required2 = arm.required ?? [];
1677
+ for (const [name, schema] of Object.entries(properties2)) {
1678
+ if (name === key) continue;
1679
+ const field = fields.get(name) ?? { shapes: [], notes: [], usedBy: [], requiredBy: [] };
1680
+ const { description, ...shape } = schema;
1681
+ if (!field.shapes.some((seen) => JSON.stringify(seen) === JSON.stringify(shape))) {
1682
+ field.shapes.push(shape);
1683
+ }
1684
+ if (typeof description === "string" && !field.notes.includes(description)) {
1685
+ field.notes.push(description);
1686
+ }
1687
+ field.usedBy.push(opId);
1688
+ if (required2.includes(name)) field.requiredBy.push(opId);
1689
+ fields.set(name, field);
1690
+ }
352
1691
  }
353
- if (Array.isArray(authRedacted)) return authRedacted.map((item) => maskSensitive(item, parentKey));
354
- if (authRedacted && typeof authRedacted === "object") {
355
- return Object.fromEntries(
356
- Object.entries(authRedacted).map(([key, child]) => [key, maskSensitive(child, key)])
1692
+ const perAction = ops.map((opId) => {
1693
+ const taken = [...fields].filter(([, field]) => field.usedBy.includes(opId));
1694
+ const listed = taken.map(
1695
+ ([name, field]) => field.requiredBy.includes(opId) ? name : `${name} (optional)`
357
1696
  );
1697
+ return `${opId} takes ${listed.length === 0 ? "no other field" : listed.join(", ")}.`;
1698
+ });
1699
+ const properties = {
1700
+ [key]: {
1701
+ type: "string",
1702
+ enum: ops,
1703
+ description: [actionNote, ...perAction].filter(Boolean).join(" ")
1704
+ }
1705
+ };
1706
+ const required = [key];
1707
+ for (const [name, field] of fields) {
1708
+ const scope = field.usedBy.length === ops.length ? "every action" : `${field.usedBy.join(" or ")} only`;
1709
+ const need = field.requiredBy.length === 0 ? "optional" : field.requiredBy.length === field.usedBy.length ? "required" : `required for ${field.requiredBy.join(" or ")}`;
1710
+ const shape = field.shapes.length === 1 ? field.shapes[0] : { anyOf: field.shapes };
1711
+ const description = [...field.notes, `For ${scope}; ${need}.`].join(" ");
1712
+ properties[name] = { ...shape, description };
1713
+ if (field.requiredBy.length === ops.length) required.push(name);
358
1714
  }
359
- return authRedacted;
1715
+ return {
1716
+ ...rest,
1717
+ type: "object",
1718
+ properties,
1719
+ required,
1720
+ ...strict ? { additionalProperties: false } : {}
1721
+ };
360
1722
  }
361
- async function readConfig(configPath) {
362
- const path = (0, import_node_path.resolve)(configPath ?? "agent-qa.config.yaml");
363
- const content = await (0, import_promises.readFile)(path, "utf-8");
364
- const raw = (0, import_yaml.parse)(content) ?? {};
365
- const validation = import_agent_qa_core2.AgentQaConfigSchema.safeParse(raw);
366
- const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
367
- const registry = record.registry && typeof record.registry === "object" && !Array.isArray(record.registry) ? record.registry : {};
368
- const services = record.services && typeof record.services === "object" && !Array.isArray(record.services) ? record.services : {};
1723
+ var McpProjectionError = class extends Error {
1724
+ constructor(toolName, detail) {
1725
+ super(`${toolName} ${detail}`);
1726
+ this.toolName = toolName;
1727
+ this.name = "McpProjectionError";
1728
+ }
1729
+ };
1730
+
1731
+ // src/resources.ts
1732
+ var import_server2 = require("@modelcontextprotocol/server");
1733
+ var import_agent_qa_agent2 = require("@vostride/agent-qa-agent");
1734
+ var import_agent_qa_schema4 = require("@vostride/agent-qa-schema");
1735
+ var ENTITIES_GET_OP_ID = "entities.get";
1736
+ var ENTITY_RESOURCE_MIME_TYPE = "application/json";
1737
+ function templateFor(kind) {
1738
+ const segment = import_agent_qa_schema4.ENTITY_URI_SEGMENTS[kind];
369
1739
  return {
370
- path,
371
- raw: maskSensitive(raw),
372
- valid: validation.success,
373
- issues: validation.success ? [] : validation.error.issues.map((issue) => ({
374
- path: issue.path.map(String).join(".") || "(root)",
375
- message: issue.message
376
- })),
377
- summary: {
378
- workspaceConfigured: Boolean(record.workspace),
379
- serviceKeys: Object.keys(services),
380
- targetNames: Object.keys(registry.targets ?? {}),
381
- deviceNames: Object.keys(registry.devices ?? {}),
382
- providerNames: Object.keys(registry.providers ?? {}),
383
- llmNames: Array.isArray(registry.llms) ? registry.llms.map((llm) => llm && typeof llm === "object" ? llm.name : void 0).filter((name) => typeof name === "string") : []
384
- }
1740
+ kind,
1741
+ segment,
1742
+ name: `agent_qa_entity_${segment}`,
1743
+ uriTemplate: `agentqa://${segment}/{id}`,
1744
+ title: `agent-qa ${segment} entity`,
1745
+ // Authored prose about a KIND, not about any entity's content — the same class of string as a
1746
+ // tool's curated title, so it is not masked. Nothing interpolated here comes from storage.
1747
+ description: `The entity card for one ${segment} addressed by its canonical agentqa:// URI (for example agentqa://${segment}/${kind}-001). Resolved through the entities read op.`
385
1748
  };
386
1749
  }
387
- function createAgentQaMcpServer(options = {}) {
388
- const server = new import_mcp.McpServer({
389
- name: "agent-qa",
390
- version: (0, import_agent_qa_core2.getAgentQaVersion)()
391
- });
392
- const analyticsService = options.analyticsService ?? (0, import_agent_qa_core2.createAnalyticsService)({ config: options.analyticsConfig });
393
- const analyticsTransport = options.transport ?? "stdio";
394
- const registerAnalyticsTool = (name, definition, handler) => server.registerTool(name, definition, (async (input, extra) => {
395
- const startedAt = Date.now();
396
- let result;
397
- let thrown;
1750
+ var ENTITY_RESOURCE_TEMPLATES = import_agent_qa_schema4.ENTITY_KINDS.map(templateFor);
1751
+ var EntityResourceUriError = class extends Error {
1752
+ constructor(detail) {
1753
+ super(`${detail} is not a canonical agentqa:// entity address`);
1754
+ this.name = "EntityResourceUriError";
1755
+ }
1756
+ };
1757
+ var EntityResourceReadError = class extends Error {
1758
+ constructor(message) {
1759
+ super(message);
1760
+ this.name = "EntityResourceReadError";
1761
+ }
1762
+ };
1763
+ function createEntityResourceReadHandler(deps) {
1764
+ return async (uri) => {
1765
+ const parsed = (0, import_agent_qa_schema4.parseEntityUri)(uri.href);
1766
+ if (parsed === void 0) {
1767
+ throw new EntityResourceUriError(deps.maskText(uri.href));
1768
+ }
1769
+ let card;
398
1770
  try {
399
- result = await handler(input, extra);
400
- return result;
1771
+ card = await deps.invoke(ENTITIES_GET_OP_ID, { ref: parsed.shortId });
401
1772
  } catch (err) {
402
- thrown = err;
403
- throw err;
404
- } finally {
405
- const isError = thrown !== void 0 || isErrorResult(result) || isDashboardErrorResult(result);
406
- const explicitRunId = extractExplicitCanonicalRunId(input);
407
- const enqueueRunId = !isError && (name === "agent_qa_enqueue_test_run" || name === "agent_qa_enqueue_suite_run") ? extractSuccessfulEnqueueRunId(result) : void 0;
408
- try {
409
- if (options.analyticsConfig?.analytics?.privacy !== true) {
410
- const standardProperties = options.analyticsStandardProperties ?? await (0, import_agent_qa_core2.resolveAnalyticsStandardProperties)({ surface: "mcp" });
411
- const event = (0, import_agent_qa_core2.buildAnalyticsEvent)({
412
- name: "agent-qa.mcp.tool.invoked",
413
- properties: {
414
- ...standardProperties,
415
- surface: standardProperties.surface ?? "mcp",
416
- tool_name: name,
417
- mcp_tool_category: categorizeMcpTool(name),
418
- mcp_tool_status: isError ? "error" : "success",
419
- duration_ms: Math.max(0, Date.now() - startedAt),
420
- mcp_transport: analyticsTransport,
421
- ...isError ? { mcp_error_category: categorizeMcpError(thrown ?? result) } : {},
422
- ...explicitRunId ?? enqueueRunId ? { run_id: explicitRunId ?? enqueueRunId } : {}
423
- }
424
- });
425
- if (options.analyticsService) {
426
- await analyticsService.capture(event).catch(() => {
427
- });
428
- } else {
429
- await (0, import_agent_qa_core2.captureAnalytics)(event, {
430
- config: options.analyticsConfig,
431
- surface: "mcp"
432
- }).catch(() => {
433
- });
434
- }
1773
+ throw new EntityResourceReadError(
1774
+ deps.maskText(err instanceof Error ? err.message : String(err))
1775
+ );
1776
+ }
1777
+ const masked = maskJsonDocument(card, deps.maskText);
1778
+ if (masked === void 0) {
1779
+ throw new EntityResourceReadError(
1780
+ "This entity could not be returned: part of its card could not be masked in place, so the unmasked record was withheld rather than served."
1781
+ );
1782
+ }
1783
+ return {
1784
+ contents: [
1785
+ {
1786
+ // Re-FORMATTED through the codec rather than echoed: the address a caller reads back is
1787
+ // the canonical one, so `agentqa://memory/MEM-1` answers about `MEM-001` and says so.
1788
+ uri: (0, import_agent_qa_schema4.formatEntityUri)(parsed.kind, parsed.shortId),
1789
+ mimeType: ENTITY_RESOURCE_MIME_TYPE,
1790
+ text: masked.text
435
1791
  }
436
- } catch {
437
- }
1792
+ ]
1793
+ };
1794
+ };
1795
+ }
1796
+ function registerEntityResources(server, deps) {
1797
+ const declaration = deps.registry.list(deps.caller).find((candidate) => candidate.id === ENTITIES_GET_OP_ID);
1798
+ if (declaration === void 0) return [];
1799
+ const read = createEntityResourceReadHandler({
1800
+ // The wall, bound once: every template's read crosses `invokeFunction` with this host's
1801
+ // registry and this caller, so input validation and the binding check run exactly as they do
1802
+ // for a tool call.
1803
+ invoke: (opId, input) => (0, import_agent_qa_agent2.invokeFunction)(deps.registry, opId, input, deps.caller),
1804
+ maskText: deps.maskText
1805
+ });
1806
+ for (const template of ENTITY_RESOURCE_TEMPLATES) {
1807
+ server.registerResource(
1808
+ template.name,
1809
+ // `list: undefined` is REQUIRED to be spelled out by the SDK, and the value is the honest
1810
+ // one: enumerating every entity of a kind is a listing this surface does not offer, and the
1811
+ // SDK asks for the explicit `undefined` precisely so that absence is a decision.
1812
+ new import_server2.ResourceTemplate(template.uriTemplate, { list: void 0 }),
1813
+ // v2 requires the metadata argument; it is not optional the way it was in v1.
1814
+ {
1815
+ title: template.title,
1816
+ description: template.description,
1817
+ mimeType: ENTITY_RESOURCE_MIME_TYPE
1818
+ },
1819
+ read
1820
+ );
1821
+ }
1822
+ return ENTITY_RESOURCE_TEMPLATES;
1823
+ }
1824
+
1825
+ // src/catalog.ts
1826
+ var AGENT_QA_MCP_SERVER_VERSION = "2.0.0";
1827
+ function resolveSpecs(projection, declarations) {
1828
+ const specs = [];
1829
+ for (const opId of projection.ops) {
1830
+ const resolved = declarations.get(opId);
1831
+ if (resolved === void 0) return void 0;
1832
+ specs.push(resolved);
1833
+ }
1834
+ return specs;
1835
+ }
1836
+ function selectFoldArm(projection, args) {
1837
+ const key = projection.discriminant;
1838
+ const record = args ?? {};
1839
+ const selected = record[key];
1840
+ if (typeof selected !== "string" || !projection.ops.includes(selected)) {
1841
+ throw new McpProjectionError(
1842
+ projection.name,
1843
+ `was called with ${key}=${JSON.stringify(selected)}, which is not one of the ops it declares`
1844
+ );
1845
+ }
1846
+ const { [key]: _arm, ...input } = record;
1847
+ return { opId: selected, input };
1848
+ }
1849
+ function resolvedCardId(resolution) {
1850
+ if (typeof resolution !== "object" || resolution === null) return void 0;
1851
+ const record = resolution;
1852
+ if (record.kind !== "resolved") return void 0;
1853
+ const card = record.card;
1854
+ return typeof card?.id === "string" ? card.id : void 0;
1855
+ }
1856
+ async function runProjection(projection, args, invoke) {
1857
+ switch (compositionOf(projection)) {
1858
+ case "single":
1859
+ return { opId: projection.ops[0], output: await invoke(projection.ops[0], args) };
1860
+ case "union": {
1861
+ const arm = selectFoldArm(projection, args);
1862
+ return { opId: arm.opId, output: await invoke(arm.opId, arm.input) };
438
1863
  }
439
- }));
440
- const dashboardUrlField = import_zod.z.string().url().optional().describe("Dashboard base URL; defaults to the MCP server dashboard URL.");
441
- const dashboardUrlFor = (inputUrl) => {
442
- const dashboardUrl = inputUrl ?? options.dashboardUrl;
443
- if (!dashboardUrl) {
444
- throw new Error("dashboardUrl is required for authoring tools. Start MCP via `agent-qa dashboard` or pass dashboardUrl.");
1864
+ case "pipeline": {
1865
+ const resolution = await invoke(projection.ops[0], args);
1866
+ const id = resolvedCardId(resolution);
1867
+ if (id === void 0) return { opId: projection.ops[0], output: resolution };
1868
+ const card = await invoke(projection.ops[1], { ref: id });
1869
+ return { opId: projection.ops[1], output: { ...resolution, card } };
445
1870
  }
446
- return dashboardUrl;
1871
+ }
1872
+ }
1873
+ function carrierRunIdOf(projection, args, output) {
1874
+ const source = projection.ui?.runIdFrom === "output" ? output : args;
1875
+ const runId = source?.runId;
1876
+ return typeof runId === "string" && runId !== "" ? runId : void 0;
1877
+ }
1878
+ function decorateCarrierResult(input) {
1879
+ if (input.projection.ui === void 0) return input.result;
1880
+ if (input.result.isError === true) return input.result;
1881
+ const runId = carrierRunIdOf(input.projection, input.args, input.output);
1882
+ const line = liveLinkLineFor(input.liveLink?.dashboardBaseUrl(), runId);
1883
+ return {
1884
+ ...input.result,
1885
+ content: [...input.result.content, { type: "text", text: input.maskText(line) }],
1886
+ ...runId === void 0 ? {} : { _meta: { ...input.result._meta, ui: { resourceUri: appResourceUriFor(runId) } } }
447
1887
  };
448
- const dashboardTool = async (dashboardUrl, path, method, body) => {
1888
+ }
1889
+ function createProjectionHandler(projection, invoke, maskText, liveLink) {
1890
+ return async (args) => {
449
1891
  try {
450
- const result = await callDashboardApi({
451
- dashboardUrl: dashboardUrlFor(dashboardUrl),
452
- path,
453
- method,
454
- body
1892
+ const { opId, output } = await runProjection(projection, args, invoke);
1893
+ const masked = maskedToolResult(output, maskText);
1894
+ const result = opId === import_agent_qa_agent3.RUNS_ARTIFACT_FUNCTION_ID ? decorateRunEvidenceImage({ result: masked, output, maskText }) : masked;
1895
+ return decorateCarrierResult({
1896
+ result,
1897
+ projection,
1898
+ args,
1899
+ output,
1900
+ maskText,
1901
+ ...liveLink === void 0 ? {} : { liveLink }
455
1902
  });
456
- return jsonContent((0, import_agent_qa_core2.redactAuthStateValue)(result));
457
1903
  } catch (err) {
458
- return errorContent(err instanceof Error ? err.message : String(err));
1904
+ return maskedToolError(err, maskText);
459
1905
  }
460
1906
  };
461
- registerAnalyticsTool(
462
- "agent_qa_discover",
463
- {
464
- title: "Discover agent-qa MCP capabilities",
465
- description: "Return structured metadata for agent-qa MCP tools, resources, prompts, and current rollout scope.",
466
- inputSchema: {}
467
- },
468
- async () => jsonContent({
469
- ...DISCOVERY,
470
- endpoint: options.endpointUrl,
471
- dashboard: options.dashboardUrl,
472
- configPath: options.configPath
473
- })
474
- );
475
- registerAnalyticsTool(
476
- "agent_qa_get_config",
477
- {
478
- title: "Inspect agent-qa config",
479
- description: "Read the active agent-qa config and return a masked raw config plus targets/devices/providers summary.",
480
- inputSchema: {
481
- configPath: import_zod.z.string().optional().describe("Config path; defaults to the MCP server config path or agent-qa.config.yaml"),
482
- section: import_zod.z.enum(["raw", "summary"]).optional().describe("Return full masked raw config or summary only")
483
- }
484
- },
485
- async ({ configPath, section }) => {
486
- const config = await readConfig(configPath ?? options.configPath);
487
- return jsonContent(section === "summary" ? { path: config.path, valid: config.valid, issues: config.issues, summary: config.summary } : config);
1907
+ }
1908
+ var TOOLS_CALL_METHOD = "tools/call";
1909
+ function callToolName(request) {
1910
+ if (typeof request !== "object" || request === null) return void 0;
1911
+ const params = request.params;
1912
+ if (typeof params !== "object" || params === null) return void 0;
1913
+ const name = params.name;
1914
+ return typeof name === "string" ? name : void 0;
1915
+ }
1916
+ function v1MigrationEntryFor(toolName) {
1917
+ return V1_TOOL_MIGRATION_MAP.find((entry) => entry.v1Name === toolName);
1918
+ }
1919
+ function v1RedirectFor(toolName, registered) {
1920
+ if (registered.has(toolName)) return void 0;
1921
+ return v1MigrationEntryFor(toolName);
1922
+ }
1923
+ function v1InputShapeNote(projection) {
1924
+ switch (compositionOf(projection)) {
1925
+ case "single":
1926
+ return "it takes one input object";
1927
+ case "union":
1928
+ return `set \`${projection.discriminant}\` to one of ${projection.ops.map((op) => `\`${op}\``).join(", ")} and pass that arm's own fields`;
1929
+ case "pipeline":
1930
+ return "it takes one entity reference and answers the card that reference resolves to";
1931
+ }
1932
+ }
1933
+ function v1RedirectMessage(entry, visibleTarget) {
1934
+ const opening = `\`${entry.v1Name}\` is a retired agent-qa v1 tool name and is not served.`;
1935
+ if (entry.arm === "renamed-to") {
1936
+ if (visibleTarget === void 0) {
1937
+ return `${opening} Call \`tools/list\` for the tools you may call on this server, and \`agent-qa init --agent\` for this workspace's capability manifest, which carries the v1 tool-name migration map.`;
488
1938
  }
489
- );
490
- registerAnalyticsTool(
491
- "agent_qa_schema_reference",
492
- {
493
- title: "Get agent-qa schema reference",
494
- description: "Return structured references for config, test YAML, suite YAML, hooks, or canonical IDs.",
495
- inputSchema: {
496
- schema: import_zod.z.enum(SCHEMA_REFERENCE_NAMES).describe("Reference name to return")
497
- }
498
- },
499
- async ({ schema }) => jsonContent(AGENT_QA_SCHEMA_REFERENCES[schema])
500
- );
501
- registerAnalyticsTool(
502
- "agent_qa_validate_definition",
503
- {
504
- title: "Validate agent-qa definition",
505
- description: "Validate config/test/suite/hooks objects with the same core schemas agent-qa uses.",
506
- inputSchema: {
507
- kind: import_zod.z.enum(VALIDATION_KINDS).describe("Definition kind"),
508
- definition: import_zod.z.unknown().describe("Parsed config/test/suite/hooks object")
509
- }
510
- },
511
- async ({ kind, definition }) => jsonContent(validateAgentQaDefinition(kind, definition))
512
- );
513
- registerAnalyticsTool(
514
- "agent_qa_generate_id",
1939
+ return `${opening} The capability it named is now \`${visibleTarget.name}\`. ${entry.note} Its input shape is not v1's \u2014 ${v1InputShapeNote(visibleTarget)}; \`tools/list\` advertises the contract the call is validated against.`;
1940
+ }
1941
+ if (entry.arm === "pending") {
1942
+ const owned = entry.owner === void 0 ? "" : ` The capability it named is a known gap owned by ${entry.owner}.`;
1943
+ const until = entry.route === void 0 ? "" : ` Until then: ${entry.route}`;
1944
+ return `${opening}${owned} ${entry.note}${until}`;
1945
+ }
1946
+ return `${opening} ${entry.note}${entry.route === void 0 ? "" : ` Instead: ${entry.route}`}`;
1947
+ }
1948
+ function installV1NameRedirect(server, deps) {
1949
+ const dispatch = server.server;
1950
+ const sdkHandler = dispatch._requestHandlers?.get(TOOLS_CALL_METHOD);
1951
+ if (typeof sdkHandler !== "function") {
1952
+ throw new McpProjectionError(
1953
+ TOOLS_CALL_METHOD,
1954
+ "has no installed request handler to wrap; the SDK no longer registers tool dispatch where this package reads it, so the retired-v1-name redirect cannot be placed"
1955
+ );
1956
+ }
1957
+ dispatch.removeRequestHandler(TOOLS_CALL_METHOD);
1958
+ dispatch.setRequestHandler(TOOLS_CALL_METHOD, (request, ctx) => {
1959
+ const name = callToolName(request);
1960
+ const entry = name === void 0 ? void 0 : v1RedirectFor(name, deps.registered);
1961
+ if (entry === void 0) return sdkHandler(request, ctx);
1962
+ const target = entry.arm === "renamed-to" && entry.v2Tool !== void 0 && deps.registered.has(entry.v2Tool) ? AGENT_QA_TOOL_PROJECTIONS.find((projection) => projection.name === entry.v2Tool) : void 0;
1963
+ throw new import_server3.ProtocolError(
1964
+ import_server3.ProtocolErrorCode.InvalidParams,
1965
+ deps.maskText(v1RedirectMessage(entry, target))
1966
+ );
1967
+ });
1968
+ }
1969
+ function buildAgentQaMcpServer(deps) {
1970
+ const server = new import_server3.McpServer(
1971
+ { name: "agent-qa", version: AGENT_QA_MCP_SERVER_VERSION },
515
1972
  {
516
- title: "Generate canonical agent-qa ID",
517
- description: "Generate a canonical id-agent backed ID for a test, suite, hook, run, or observation.",
518
- inputSchema: {
519
- type: import_zod.z.enum(import_agent_qa_ids2.ENTITY_ID_TYPES).describe("ID entity type")
520
- }
521
- },
522
- async ({ type }) => {
523
- const idType = type;
524
- return jsonContent({
525
- id: (0, import_agent_qa_ids2.generateCanonicalId)(idType),
526
- type: idType,
527
- contract: (0, import_agent_qa_ids2.getEntityIdContract)(idType)
528
- });
1973
+ capabilities: {
1974
+ tools: {},
1975
+ resources: {},
1976
+ // 408 D-04: the official MCP Apps extension, ADVERTISED rather than assumed. Extensions are
1977
+ // bilateral opt-in — a client declares support in the `io.modelcontextprotocol/
1978
+ // clientCapabilities` envelope it sends on every request, and reads the server's set back
1979
+ // from discovery — so a server that renders App HTML without naming the extension here is
1980
+ // one no host will ever ask for it. The value is an empty record because this extension
1981
+ // carries no server-side options; what it says is that this endpoint speaks it at all.
1982
+ extensions: { [MCP_APPS_EXTENSION_ID]: {} }
1983
+ },
1984
+ // 433-33 (G-433-9): init's pointer, on the one channel every MCP host receives.
1985
+ instructions: AGENT_QA_MCP_INSTRUCTIONS
529
1986
  }
530
1987
  );
531
- registerAnalyticsTool(
532
- "agent_qa_validate_id",
533
- {
534
- title: "Validate canonical agent-qa ID",
535
- description: "Validate canonical id-agent backed IDs and return the expected contract.",
536
- inputSchema: {
537
- type: import_zod.z.enum(import_agent_qa_ids2.ENTITY_ID_TYPES).describe("ID entity type"),
538
- id: import_zod.z.string().describe("ID to validate")
539
- }
540
- },
541
- async ({ type, id }) => {
542
- const idType = type;
543
- const contract = (0, import_agent_qa_ids2.getEntityIdContract)(idType);
544
- const valid = (0, import_agent_qa_ids2.isCanonicalId)(idType, id);
545
- return jsonContent({
546
- valid,
547
- id,
548
- type: idType,
549
- contract,
550
- message: valid ? "ID is canonical." : `Expected ${contract.prefixWithSeparator} followed by ${contract.words} id-agent words.`
551
- });
1988
+ const invoke = (opId, input) => (0, import_agent_qa_agent3.invokeFunction)(deps.registry, opId, input, deps.caller);
1989
+ const declarations = /* @__PURE__ */ new Map();
1990
+ for (const declaration of deps.registry.list(deps.caller)) {
1991
+ const contracts = AGENT_QA_OP_CONTRACTS[declaration.id];
1992
+ if (contracts === void 0) continue;
1993
+ declarations.set(declaration.id, {
1994
+ id: declaration.id,
1995
+ summary: declaration.summary,
1996
+ risk: declaration.risk,
1997
+ input: contracts.input,
1998
+ output: contracts.output
1999
+ });
2000
+ }
2001
+ const registered = /* @__PURE__ */ new Set();
2002
+ for (const projection of AGENT_QA_TOOL_PROJECTIONS) {
2003
+ if (registered.has(projection.name)) {
2004
+ throw new McpProjectionError(
2005
+ projection.name,
2006
+ "is declared twice in the mapping table; another projection already occupies that name"
2007
+ );
552
2008
  }
553
- );
554
- registerAnalyticsTool(
555
- "agent_qa_list_tests",
556
- {
557
- title: "List agent-qa tests",
558
- description: "List tests through the dashboard API using dashboard workspace discovery.",
559
- inputSchema: { dashboardUrl: dashboardUrlField }
560
- },
561
- async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/tests")
562
- );
563
- registerAnalyticsTool(
564
- "agent_qa_read_test",
565
- {
566
- title: "Read agent-qa test",
567
- description: "Read a test by canonical test ID through the dashboard API.",
568
- inputSchema: {
569
- dashboardUrl: dashboardUrlField,
570
- testId: import_zod.z.string().describe("Canonical t_ test ID")
571
- }
572
- },
573
- async ({ dashboardUrl, testId }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`)
574
- );
575
- registerAnalyticsTool(
576
- "agent_qa_validate_test",
577
- {
578
- title: "Validate agent-qa test YAML",
579
- description: "Validate test YAML through the dashboard test-file manager.",
580
- inputSchema: {
581
- dashboardUrl: dashboardUrlField,
582
- content: import_zod.z.string().describe("Test YAML content"),
583
- filePath: import_zod.z.string().optional().describe("Optional file path for suite/test routing hints")
584
- }
585
- },
586
- async ({ dashboardUrl, content, filePath }) => dashboardTool(dashboardUrl, "/api/tests/validate", "POST", { content, filePath })
587
- );
588
- registerAnalyticsTool(
589
- "agent_qa_create_test",
590
- {
591
- title: "Create agent-qa test",
592
- description: "Create a test file through the dashboard workspace-safe file manager.",
593
- inputSchema: {
594
- dashboardUrl: dashboardUrlField,
595
- path: import_zod.z.string().describe("Workspace-relative test path matched by workspace.testMatch"),
596
- content: import_zod.z.string().describe("Test YAML content")
597
- }
598
- },
599
- async ({ dashboardUrl, path, content }) => dashboardTool(dashboardUrl, "/api/tests", "POST", { path, content })
600
- );
601
- registerAnalyticsTool(
602
- "agent_qa_update_test",
603
- {
604
- title: "Update agent-qa test",
605
- description: "Update a test by canonical test ID through the dashboard API.",
606
- inputSchema: {
607
- dashboardUrl: dashboardUrlField,
608
- testId: import_zod.z.string().describe("Canonical t_ test ID"),
609
- content: import_zod.z.string().describe("Replacement test YAML content")
610
- }
611
- },
612
- async ({ dashboardUrl, testId, content }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`, "PUT", { content })
613
- );
614
- registerAnalyticsTool(
615
- "agent_qa_delete_test",
616
- {
617
- title: "Delete agent-qa test",
618
- description: "Delete a test by canonical test ID through the dashboard API.",
619
- inputSchema: {
620
- dashboardUrl: dashboardUrlField,
621
- testId: import_zod.z.string().describe("Canonical t_ test ID")
622
- }
623
- },
624
- async ({ dashboardUrl, testId }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`, "DELETE")
625
- );
626
- registerAnalyticsTool(
627
- "agent_qa_list_suites",
628
- {
629
- title: "List agent-qa suites",
630
- description: "List suites through the dashboard API using dashboard workspace discovery.",
631
- inputSchema: { dashboardUrl: dashboardUrlField }
632
- },
633
- async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/suites")
634
- );
635
- registerAnalyticsTool(
636
- "agent_qa_read_suite",
637
- {
638
- title: "Read agent-qa suite",
639
- description: "Read a suite by canonical suite ID through the dashboard API.",
640
- inputSchema: {
641
- dashboardUrl: dashboardUrlField,
642
- suiteId: import_zod.z.string().describe("Canonical s_ suite ID")
643
- }
644
- },
645
- async ({ dashboardUrl, suiteId }) => dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(suiteId)}`)
646
- );
647
- registerAnalyticsTool(
648
- "agent_qa_validate_suite",
649
- {
650
- title: "Validate agent-qa suite YAML",
651
- description: "Validate suite YAML through the dashboard suite-file manager.",
652
- inputSchema: {
653
- dashboardUrl: dashboardUrlField,
654
- content: import_zod.z.string().describe("Suite YAML content")
655
- }
656
- },
657
- async ({ dashboardUrl, content }) => dashboardTool(dashboardUrl, "/api/suites/validate", "POST", { content })
658
- );
659
- registerAnalyticsTool(
660
- "agent_qa_create_suite",
661
- {
662
- title: "Create agent-qa suite",
663
- description: "Create a suite file through the dashboard workspace-safe suite manager.",
664
- inputSchema: {
665
- dashboardUrl: dashboardUrlField,
666
- path: import_zod.z.string().describe("Workspace-relative suite path matched by workspace.suiteMatch"),
667
- content: import_zod.z.string().describe("Suite YAML content")
668
- }
669
- },
670
- async ({ dashboardUrl, path, content }) => dashboardTool(dashboardUrl, "/api/suites", "POST", { path, content })
671
- );
672
- registerAnalyticsTool(
673
- "agent_qa_update_suite",
674
- {
675
- title: "Update agent-qa suite",
676
- description: "Update a suite by canonical suite ID through the dashboard API.",
677
- inputSchema: {
678
- dashboardUrl: dashboardUrlField,
679
- suiteId: import_zod.z.string().describe("Canonical s_ suite ID"),
680
- content: import_zod.z.string().describe("Replacement suite YAML content")
681
- }
682
- },
683
- async ({ dashboardUrl, suiteId, content }) => dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(suiteId)}`, "PUT", { content })
684
- );
685
- registerAnalyticsTool(
686
- "agent_qa_delete_suite",
687
- {
688
- title: "Delete agent-qa suite",
689
- description: "Delete a suite through the dashboard API. Prefer suiteId; path is supported for the dashboard delete route.",
690
- inputSchema: {
691
- dashboardUrl: dashboardUrlField,
692
- suiteId: import_zod.z.string().optional().describe("Canonical s_ suite ID"),
693
- path: import_zod.z.string().optional().describe("Workspace-relative suite path")
694
- }
695
- },
696
- async ({ dashboardUrl, suiteId, path }) => {
697
- if (path) return dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(path)}`, "DELETE");
698
- if (!suiteId) return errorContent("Either suiteId or path is required.");
699
- let readResult;
700
- try {
701
- readResult = await callDashboardApi({
702
- dashboardUrl: dashboardUrlFor(dashboardUrl),
703
- path: `/api/suites/${encodeURIComponent(suiteId)}`
704
- });
705
- } catch (err) {
706
- return errorContent(err instanceof Error ? err.message : String(err));
2009
+ for (const opId of projection.ops) {
2010
+ if (AGENT_QA_OP_CONTRACTS[opId] === void 0) {
2011
+ throw new McpProjectionError(
2012
+ projection.name,
2013
+ `names op ${opId}, which has no entry in the contract table`
2014
+ );
707
2015
  }
708
- if (!readResult.ok || !readResult.body || typeof readResult.body !== "object") return jsonContent(readResult);
709
- const foundPath = readResult.body.path;
710
- if (typeof foundPath !== "string") return errorContent("Suite read response did not include a path.");
711
- return dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(foundPath)}`, "DELETE");
712
2016
  }
713
- );
714
- registerAnalyticsTool(
715
- "agent_qa_list_hooks",
716
- {
717
- title: "List agent-qa hooks",
718
- description: "List hooks through the dashboard hook registry API.",
719
- inputSchema: { dashboardUrl: dashboardUrlField }
720
- },
721
- async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/hooks")
722
- );
723
- registerAnalyticsTool(
724
- "agent_qa_read_hook",
725
- {
726
- title: "Read agent-qa hook",
727
- description: "Read hook metadata and source by canonical hook ID.",
728
- inputSchema: {
729
- dashboardUrl: dashboardUrlField,
730
- hookId: import_zod.z.string().describe("Canonical h_ hook ID")
731
- }
732
- },
733
- async ({ dashboardUrl, hookId }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}`)
734
- );
735
- registerAnalyticsTool(
736
- "agent_qa_create_hook",
737
- {
738
- title: "Create agent-qa hook",
739
- description: "Create a hook through the dashboard hook registry API.",
740
- inputSchema: {
741
- dashboardUrl: dashboardUrlField,
742
- payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).describe("Dashboard HookMutationRequest payload")
743
- }
744
- },
745
- async ({ dashboardUrl, payload }) => dashboardTool(dashboardUrl, "/api/hooks", "POST", payload)
746
- );
747
- registerAnalyticsTool(
748
- "agent_qa_update_hook",
749
- {
750
- title: "Update agent-qa hook",
751
- description: "Update a hook through the dashboard hook registry API.",
752
- inputSchema: {
753
- dashboardUrl: dashboardUrlField,
754
- hookId: import_zod.z.string().describe("Canonical h_ hook ID"),
755
- payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).describe("Dashboard HookMutationRequest payload")
756
- }
757
- },
758
- async ({ dashboardUrl, hookId, payload }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}`, "PUT", payload)
759
- );
760
- registerAnalyticsTool(
761
- "agent_qa_delete_hook",
762
- {
763
- title: "Delete agent-qa hook",
764
- description: "Delete a hook through the dashboard hook registry API.",
765
- inputSchema: {
766
- dashboardUrl: dashboardUrlField,
767
- hookId: import_zod.z.string().describe("Canonical h_ hook ID"),
768
- force: import_zod.z.boolean().optional().describe("Delete even when references exist")
769
- }
770
- },
771
- async ({ dashboardUrl, hookId, force }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}${force ? "?force=true" : ""}`, "DELETE")
772
- );
773
- registerAnalyticsTool(
774
- "agent_qa_run_hook",
775
- {
776
- title: "Run agent-qa hook",
777
- description: "Run a hook through the dashboard hook execution API.",
778
- inputSchema: {
779
- dashboardUrl: dashboardUrlField,
780
- hookId: import_zod.z.string().describe("Canonical h_ hook ID"),
781
- payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional().describe("Dashboard HookRunRequest payload")
782
- }
783
- },
784
- async ({ dashboardUrl, hookId, payload }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}/run`, "POST", payload ?? {})
785
- );
786
- registerAnalyticsTool(
787
- "agent_qa_enqueue_test_run",
788
- {
789
- title: "Enqueue agent-qa test run",
790
- description: "Queue a test run through the dashboard run trigger API and return the canonical run ID.",
791
- inputSchema: {
792
- dashboardUrl: dashboardUrlField,
793
- file: import_zod.z.string().optional().describe("Workspace-relative test file path"),
794
- patterns: import_zod.z.array(import_zod.z.string()).optional().describe("Optional test patterns"),
795
- noCache: import_zod.z.boolean().optional(),
796
- noMemory: import_zod.z.boolean().optional(),
797
- local: import_zod.z.boolean().optional()
798
- }
799
- },
800
- async ({ dashboardUrl, file, patterns, noCache, noMemory, local }) => dashboardTool(dashboardUrl, "/api/runs/trigger", "POST", { file, patterns, noCache, noMemory, local, triggerSource: "mcp" })
801
- );
802
- registerAnalyticsTool(
803
- "agent_qa_enqueue_suite_run",
804
- {
805
- title: "Enqueue agent-qa suite run",
806
- description: "Queue a suite run through the dashboard run trigger API and return parent run context.",
807
- inputSchema: {
808
- dashboardUrl: dashboardUrlField,
809
- file: import_zod.z.string().describe("Workspace-relative suite file path"),
810
- noCache: import_zod.z.boolean().optional(),
811
- noMemory: import_zod.z.boolean().optional(),
812
- local: import_zod.z.boolean().optional()
813
- }
814
- },
815
- async ({ dashboardUrl, file, noCache, noMemory, local }) => dashboardTool(dashboardUrl, "/api/runs/trigger", "POST", { file, noCache, noMemory, local, triggerSource: "mcp" })
816
- );
817
- registerAnalyticsTool(
818
- "agent_qa_get_run",
819
- {
820
- title: "Get agent-qa run",
821
- description: "Get run detail, steps, attempts, and suite child context through the dashboard API.",
822
- inputSchema: {
823
- dashboardUrl: dashboardUrlField,
824
- runId: import_zod.z.string().describe("Canonical r_ run ID")
825
- }
826
- },
827
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}`)
828
- );
829
- registerAnalyticsTool(
830
- "agent_qa_get_run_steps",
831
- {
832
- title: "Get agent-qa run steps",
833
- description: "Get run steps through the dashboard API.",
834
- inputSchema: {
835
- dashboardUrl: dashboardUrlField,
836
- runId: import_zod.z.string().describe("Canonical r_ run ID")
837
- }
838
- },
839
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/steps`)
840
- );
841
- registerAnalyticsTool(
842
- "agent_qa_get_run_logs",
843
- {
844
- title: "Get agent-qa run logs",
845
- description: "Get run log rows through the dashboard API.",
846
- inputSchema: {
847
- dashboardUrl: dashboardUrlField,
848
- runId: import_zod.z.string().describe("Canonical r_ run ID"),
849
- stepId: import_zod.z.string().optional(),
850
- level: import_zod.z.string().optional(),
851
- source: import_zod.z.string().optional(),
852
- limit: import_zod.z.number().int().positive().optional(),
853
- offset: import_zod.z.number().int().min(0).optional()
854
- }
855
- },
856
- async ({ dashboardUrl, runId, stepId, level, source, limit, offset }) => dashboardTool(
857
- dashboardUrl,
858
- withQuery(`/api/runs/${encodeURIComponent(runId)}/logs`, { stepId, level, source, limit, offset })
859
- )
860
- );
861
- registerAnalyticsTool(
862
- "agent_qa_get_run_execution_logs",
863
- {
864
- title: "Get agent-qa execution logs",
865
- description: "Get structured execution logs through the dashboard API.",
866
- inputSchema: {
867
- dashboardUrl: dashboardUrlField,
868
- runId: import_zod.z.string().describe("Canonical r_ run ID"),
869
- stepId: import_zod.z.string().optional(),
870
- type: import_zod.z.string().optional()
871
- }
872
- },
873
- async ({ dashboardUrl, runId, stepId, type }) => dashboardTool(
874
- dashboardUrl,
875
- withQuery(`/api/runs/${encodeURIComponent(runId)}/execution-logs`, { stepId, type })
876
- )
877
- );
878
- registerAnalyticsTool(
879
- "agent_qa_get_run_artifact",
880
- {
881
- title: "Get agent-qa run artifact bundle",
882
- description: "Get sanitized run artifact bundle, child artifacts, and missing artifact sections.",
883
- inputSchema: {
884
- dashboardUrl: dashboardUrlField,
885
- runId: import_zod.z.string().describe("Canonical r_ run ID")
886
- }
887
- },
888
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/artifact`)
889
- );
890
- registerAnalyticsTool(
891
- "agent_qa_cancel_run",
892
- {
893
- title: "Cancel agent-qa run",
894
- description: "Cancel a pending or running run through the dashboard queue.",
895
- inputSchema: {
896
- dashboardUrl: dashboardUrlField,
897
- runId: import_zod.z.string().describe("Canonical r_ run ID")
898
- }
899
- },
900
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/cancel`, "POST", {})
901
- );
902
- registerAnalyticsTool(
903
- "agent_qa_classify_failure",
904
- {
905
- title: "Classify agent-qa failure",
906
- description: "Classify a failed run using dashboard run detail, artifacts, logs, execution logs, and recent related runs.",
907
- inputSchema: {
908
- dashboardUrl: dashboardUrlField,
909
- runId: import_zod.z.string().describe("Canonical r_ run ID")
910
- }
911
- },
912
- async ({ dashboardUrl, runId }) => {
913
- try {
914
- const baseUrl = dashboardUrlFor(dashboardUrl);
915
- const runDetail = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}` });
916
- const artifact = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/artifact` });
917
- const logs = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/logs` });
918
- const executionLogs = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/execution-logs` });
919
- const runName = runDetail.body && typeof runDetail.body === "object" && runDetail.body.run && typeof runDetail.body.run?.name === "string" ? runDetail.body.run.name : void 0;
920
- const recentRuns = runName ? await callDashboardApi({ dashboardUrl: baseUrl, path: withQuery("/api/runs", { name: runName, limit: 5 }) }) : { ok: true, status: 200, body: { runs: [] } };
921
- return jsonContent({
922
- runId,
923
- classification: classifyRunFailureFromDashboardData({
924
- runDetail: runDetail.body,
925
- artifact: artifact.body,
926
- logs: logs.body,
927
- executionLogs: executionLogs.body,
928
- recentRuns: recentRuns.body
929
- }),
930
- sources: {
931
- run: { ok: runDetail.ok, status: runDetail.status },
932
- artifact: { ok: artifact.ok, status: artifact.status },
933
- logs: { ok: logs.ok, status: logs.status },
934
- executionLogs: { ok: executionLogs.ok, status: executionLogs.status },
935
- recentRuns: { ok: recentRuns.ok, status: recentRuns.status }
936
- }
937
- });
938
- } catch (err) {
939
- return errorContent(err instanceof Error ? err.message : String(err));
940
- }
2017
+ const advertised = advertisedContracts(projection);
2018
+ if (advertised === void 0) {
2019
+ throw new McpProjectionError(
2020
+ projection.name,
2021
+ "has no advertised contract; a multi-arm tool needs an entry in AGENT_QA_COMPOSED_CONTRACTS"
2022
+ );
941
2023
  }
942
- );
943
- for (const schema of SCHEMA_REFERENCE_NAMES) {
944
- server.registerResource(
945
- `agent_qa.schema.${schema}`,
946
- `agent-qa://schema/${schema}`,
947
- {
948
- title: `agent-qa ${schema} schema reference`,
949
- description: `Structured agent-qa reference for ${schema}.`,
950
- mimeType: "application/json"
951
- },
952
- async (uri) => ({
953
- contents: [{
954
- uri: uri.href,
955
- mimeType: "application/json",
956
- text: JSON.stringify(AGENT_QA_SCHEMA_REFERENCES[schema], null, 2)
957
- }]
958
- })
2024
+ const specs = resolveSpecs(projection, declarations);
2025
+ if (specs === void 0) continue;
2026
+ registered.add(projection.name);
2027
+ const config = toRegisterToolConfig(projection, specs, advertised, deps.maskText);
2028
+ server.registerTool(
2029
+ projection.name,
2030
+ config,
2031
+ createProjectionHandler(projection, invoke, deps.maskText, deps.liveLink)
959
2032
  );
960
2033
  }
961
- server.registerPrompt(
962
- "agent_qa_authoring_context",
963
- {
964
- title: "agent-qa authoring context",
965
- description: "Prompt context for agents writing agent-qa tests and suites.",
966
- argsSchema: {}
967
- },
968
- async () => ({
969
- messages: [{
970
- role: "user",
971
- content: {
972
- type: "text",
973
- text: [
974
- "Use agent-qa MCP tools before editing files.",
975
- "Call agent_qa_get_config to inspect targets/devices/providers.",
976
- "Call agent_qa_generate_id for test, suite, hook, run, and observation IDs.",
977
- "Call agent_qa_validate_definition before saving config, test, suite, or hooks YAML.",
978
- `Canonical ID contracts: ${JSON.stringify((0, import_agent_qa_ids2.getEntityIdContracts)())}`
979
- ].join("\n")
980
- }
981
- }]
982
- })
983
- );
2034
+ installV1NameRedirect(server, { registered, maskText: deps.maskText });
2035
+ registerEntityResources(server, {
2036
+ registry: deps.registry,
2037
+ caller: deps.caller,
2038
+ maskText: deps.maskText
2039
+ });
2040
+ registerAppResources(server, {
2041
+ registry: deps.registry,
2042
+ caller: deps.caller,
2043
+ maskText: deps.maskText,
2044
+ // 408 plan 03 (D-08): the SAME supplier the carrier decoration reads, so the anchor inside the
2045
+ // App document and the line beside a tool result name one page for one run.
2046
+ ...deps.liveLink === void 0 ? {} : { liveLink: deps.liveLink }
2047
+ });
984
2048
  return server;
985
2049
  }
986
2050
 
987
- // src/server.ts
988
- var MCP_STDIO_STARTUP_MESSAGE = "agent-qa MCP server running over stdio. Waiting for MCP client messages on stdin. Stdout is reserved for MCP protocol traffic.";
989
- function createMcpServer(options = {}) {
990
- return createAgentQaMcpServer(options);
991
- }
992
- async function startMcpServer(options = {}) {
993
- const { startupOutput, ...serverOptions } = options;
994
- const server = createMcpServer({ ...serverOptions, transport: "stdio" });
995
- const transport = new import_stdio.StdioServerTransport();
996
- await server.connect(transport);
997
- try {
998
- startupOutput?.write(`${MCP_STDIO_STARTUP_MESSAGE}
999
- `);
1000
- } catch {
1001
- }
1002
- try {
1003
- if (options.analyticsConfig?.analytics?.privacy !== true) {
1004
- const standardProperties = options.analyticsStandardProperties ?? await (0, import_agent_qa_core3.resolveAnalyticsStandardProperties)({ surface: "mcp" });
1005
- const event = (0, import_agent_qa_core3.buildAnalyticsEvent)({
1006
- name: "agent-qa.mcp.server.lifecycle",
1007
- properties: {
1008
- ...standardProperties,
1009
- surface: standardProperties.surface ?? "mcp",
1010
- mcp_server_state: "started",
1011
- mcp_transport: "stdio"
1012
- }
1013
- });
1014
- if (options.analyticsService) {
1015
- await options.analyticsService.capture(event).catch(() => {
1016
- });
1017
- } else {
1018
- await (0, import_agent_qa_core3.captureAnalytics)(event, {
1019
- config: options.analyticsConfig,
1020
- surface: "mcp"
1021
- }).catch(() => {
1022
- });
1023
- }
1024
- }
1025
- } catch {
1026
- }
2051
+ // src/http.ts
2052
+ var import_server4 = require("@modelcontextprotocol/server");
2053
+ var import_node = require("@modelcontextprotocol/node");
2054
+ function createAgentQaMcpNodeHandler(resolveDeps) {
2055
+ return (0, import_node.toNodeHandler)((0, import_server4.createMcpHandler)((ctx) => buildAgentQaMcpServer(resolveDeps(ctx))));
1027
2056
  }
1028
2057
 
1029
- // src/local-http.ts
1030
- var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
1031
- var DEFAULT_MCP_HOST = "127.0.0.1";
1032
- var DEFAULT_MCP_PORT = 3471;
1033
- var DEFAULT_MCP_PATH = "/mcp";
1034
- function normalizePath(path) {
1035
- if (!path) return DEFAULT_MCP_PATH;
1036
- return path.startsWith("/") ? path : `/${path}`;
2058
+ // src/conformance-support.ts
2059
+ function withoutDialect(document) {
2060
+ const { $schema: _dialect, ...rest } = document ?? {};
2061
+ return stripRedundantObjectStamp(rest);
1037
2062
  }
1038
- function jsonRpcError(res, status, message) {
1039
- res.writeHead(status, {
1040
- "Content-Type": "application/json",
1041
- "Access-Control-Allow-Origin": "*"
1042
- });
1043
- res.end(JSON.stringify({
1044
- jsonrpc: "2.0",
1045
- error: {
1046
- code: -32e3,
1047
- message
1048
- },
1049
- id: null
1050
- }));
2063
+ function stripRedundantObjectStamp(document) {
2064
+ const composed = document.oneOf !== void 0 || document.anyOf !== void 0;
2065
+ if (!composed || document.type !== "object") return document;
2066
+ const { type: _stamp, ...rest } = document;
2067
+ return rest;
1051
2068
  }
1052
- function resolveLocalMcpEndpoint(config = {}) {
1053
- const transport = config.transport ?? "http";
1054
- const host = config.host ?? DEFAULT_MCP_HOST;
1055
- const port = config.port ?? DEFAULT_MCP_PORT;
1056
- const path = normalizePath(config.path);
1057
- const displayHost = host === "::1" ? "[::1]" : host;
1058
- return {
1059
- enabled: config.enabled !== false,
1060
- transport,
1061
- host,
1062
- port,
1063
- path,
1064
- url: `http://${displayHost}:${port}${path}`
1065
- };
1066
- }
1067
- function resolveMcpEndpointShape(endpoint) {
1068
- return {
1069
- mcp_host_kind: ["127.0.0.1", "localhost", "::1"].includes(endpoint.host) ? "loopback" : "other",
1070
- mcp_port_kind: endpoint.port === DEFAULT_MCP_PORT ? "default" : "custom",
1071
- mcp_path_kind: endpoint.path === DEFAULT_MCP_PATH ? "default" : "custom"
1072
- };
2069
+ function armWithoutDiscriminant(arm, discriminant) {
2070
+ const properties = { ...arm.properties ?? {} };
2071
+ delete properties[discriminant];
2072
+ const required = (arm.required ?? []).filter((key) => key !== discriminant);
2073
+ const rebuilt = { ...arm, properties, required };
2074
+ if (required.length === 0) delete rebuilt.required;
2075
+ return rebuilt;
1073
2076
  }
1074
- function createLocalMcpHttpHandler(options = {}) {
1075
- const endpoint = resolveLocalMcpEndpoint(options.endpoint);
1076
- const baseHost = endpoint.host === "::1" ? "[::1]" : endpoint.host;
1077
- return async (req, res) => {
1078
- const requestUrl = new URL(req.url ?? "/", `http://${baseHost}:${endpoint.port}`);
1079
- if (requestUrl.pathname !== endpoint.path) {
1080
- jsonRpcError(res, 404, `MCP endpoint is available at ${endpoint.path}`);
1081
- return;
2077
+ function foldArmFindings(listed, discriminant, opId, own) {
2078
+ const findings = [];
2079
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
2080
+ if (listed[key] !== void 0) findings.push(`the listing carries a top-level ${key}`);
2081
+ }
2082
+ if (listed.type !== "object") findings.push("the listing is not type object");
2083
+ const properties = listed.properties ?? {};
2084
+ const selector = properties[discriminant]?.enum;
2085
+ if (!Array.isArray(selector) || !selector.includes(opId)) {
2086
+ findings.push(`${discriminant} does not list ${opId}`);
2087
+ }
2088
+ const ownProperties = own.properties ?? {};
2089
+ for (const [name, field] of Object.entries(ownProperties)) {
2090
+ const shown = properties[name];
2091
+ if (shown === void 0) {
2092
+ findings.push(`${name} is not listed`);
2093
+ continue;
1082
2094
  }
1083
- if (req.method === "OPTIONS") {
1084
- res.writeHead(204, {
1085
- "Access-Control-Allow-Origin": "*",
1086
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
1087
- "Access-Control-Allow-Headers": "Content-Type, MCP-Protocol-Version, mcp-session-id"
1088
- });
1089
- res.end();
1090
- return;
2095
+ const { description: ownNote, ...ownShape } = field;
2096
+ const { description: shownNote, ...shownShape } = shown;
2097
+ const merged = Array.isArray(shownShape.anyOf) && Object.keys(shownShape).length === 1;
2098
+ const shapes = merged ? [shownShape, ...shownShape.anyOf] : [shownShape];
2099
+ if (!shapes.some((shape) => JSON.stringify(shape) === JSON.stringify(ownShape))) {
2100
+ findings.push(`${name} is listed with another shape`);
1091
2101
  }
1092
- const server = createAgentQaMcpServer({
1093
- configPath: options.configPath,
1094
- dashboardUrl: options.dashboardUrl,
1095
- endpointUrl: endpoint.url,
1096
- analyticsConfig: options.analyticsConfig,
1097
- transport: "http"
1098
- });
1099
- const transport = new import_streamableHttp.StreamableHTTPServerTransport({
1100
- sessionIdGenerator: void 0
1101
- });
1102
- try {
1103
- await server.connect(transport);
1104
- await transport.handleRequest(req, res);
1105
- res.on("close", () => {
1106
- void transport.close();
1107
- void server.close();
1108
- });
1109
- } catch (err) {
1110
- void transport.close();
1111
- void server.close();
1112
- if (!res.headersSent) {
1113
- jsonRpcError(
1114
- res,
1115
- 500,
1116
- err instanceof Error ? err.message : "Internal MCP server error"
1117
- );
1118
- }
2102
+ const kept = typeof shownNote === "string" && shownNote.includes(String(ownNote));
2103
+ if (typeof ownNote === "string" && !kept) {
2104
+ findings.push(`${name} lost its own description`);
1119
2105
  }
2106
+ }
2107
+ const allowed = /* @__PURE__ */ new Set([discriminant, ...own.required ?? []]);
2108
+ for (const name of listed.required ?? []) {
2109
+ if (!allowed.has(name)) findings.push(`the listing requires ${name}, which ${opId} does not`);
2110
+ }
2111
+ if (own.additionalProperties === false && listed.additionalProperties !== false) {
2112
+ findings.push("the listing admits fields no arm takes");
2113
+ }
2114
+ return findings;
2115
+ }
2116
+ function withoutLegacyResultWrap(document) {
2117
+ const wrapper = document ?? {};
2118
+ const keys = Object.keys(wrapper).filter((key) => key !== "$schema").sort();
2119
+ if (keys.join(",") !== "properties,required,type" || wrapper.type !== "object") return wrapper;
2120
+ const properties = wrapper.properties;
2121
+ const required = wrapper.required;
2122
+ if (Object.keys(properties ?? {}).join(",") !== "result") return wrapper;
2123
+ if (!Array.isArray(required) || required.length !== 1 || required[0] !== "result") return wrapper;
2124
+ const natural = properties.result;
2125
+ if (typeof natural !== "object" || natural === null || natural.type === "object") return wrapper;
2126
+ return natural;
2127
+ }
2128
+
2129
+ // src/task-seam.ts
2130
+ var JOB_STATUS_TO_MCP_TASK_STATE = {
2131
+ // The four checkpoint boundaries. One answer, four times, on purpose.
2132
+ queued: "working",
2133
+ authored: "working",
2134
+ bundling: "working",
2135
+ committing: "working",
2136
+ // The three terminal states, kept apart.
2137
+ done: "completed",
2138
+ failed: "failed",
2139
+ cancelled: "cancelled"
2140
+ };
2141
+ function projectJobAsTask(job) {
2142
+ return {
2143
+ taskId: job.id,
2144
+ status: JOB_STATUS_TO_MCP_TASK_STATE[job.status],
2145
+ statusMessage: job.summary,
2146
+ createdAt: job.createdAt,
2147
+ lastUpdatedAt: job.updatedAt
1120
2148
  };
1121
2149
  }
1122
2150
  // Annotate the CommonJS export names for ESM import in node:
1123
2151
  0 && (module.exports = {
1124
- AGENT_QA_SCHEMA_REFERENCES,
1125
- DEFAULT_MCP_HOST,
1126
- DEFAULT_MCP_PATH,
1127
- DEFAULT_MCP_PORT,
1128
- MCP_STDIO_STARTUP_MESSAGE,
1129
- SCHEMA_REFERENCE_NAMES,
1130
- VALIDATION_KINDS,
1131
- classifyRunFailureFromDashboardData,
1132
- createAgentQaMcpServer,
1133
- createLocalMcpHttpHandler,
1134
- createMcpServer,
1135
- resolveDashboardApiUrl,
1136
- resolveLocalMcpEndpoint,
1137
- resolveMcpEndpointShape,
1138
- startMcpServer,
1139
- validateAgentQaDefinition
2152
+ AGENT_QA_COMPOSED_CONTRACTS,
2153
+ AGENT_QA_MCP_INSTRUCTIONS,
2154
+ AGENT_QA_MCP_SERVER_VERSION,
2155
+ AGENT_QA_OP_CONTRACTS,
2156
+ AGENT_QA_TOOL_PROJECTIONS,
2157
+ APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY,
2158
+ APP_RESOURCE_MIME_TYPE,
2159
+ APP_RESOURCE_NAME,
2160
+ APP_RESOURCE_URI_TEMPLATE,
2161
+ APP_WRAPPER_APP_NAME,
2162
+ APP_WRAPPER_APP_VERSION,
2163
+ APP_WRAPPER_CONNECTING_HINT,
2164
+ APP_WRAPPER_CONNECTING_MESSAGE,
2165
+ APP_WRAPPER_EXPIRED_HINT,
2166
+ APP_WRAPPER_EXPIRED_MESSAGE,
2167
+ APP_WRAPPER_LIVE_MESSAGE,
2168
+ APP_WRAPPER_LOAD_TIMEOUT_MS,
2169
+ APP_WRAPPER_PERMANENT_LINK_LABEL,
2170
+ APP_WRAPPER_RUN_ENDED_MESSAGE,
2171
+ APP_WRAPPER_STATES,
2172
+ APP_WRAPPER_UI_PROTOCOL_VERSION,
2173
+ APP_WRAPPER_UNREACHABLE_MESSAGE,
2174
+ AppResourceNegotiationError,
2175
+ AppResourceOriginSpanError,
2176
+ AppResourceReadError,
2177
+ AppResourceUriError,
2178
+ DEGRADED_OUTPUT_CONTRACTS,
2179
+ JOB_STATUS_TO_MCP_TASK_STATE,
2180
+ LIVE_LINK_ABSENT_LINE,
2181
+ LIVE_LINK_NO_RUN_LINE,
2182
+ MCP_APPS_EXTENSION_ID,
2183
+ McpProjectionError,
2184
+ UNPROJECTED_OPS,
2185
+ V1_TOOL_MIGRATION_MAP,
2186
+ advertisedContracts,
2187
+ appResourceUriFor,
2188
+ armWithoutDiscriminant,
2189
+ buildAgentQaMcpServer,
2190
+ compositionOf,
2191
+ createAgentQaMcpNodeHandler,
2192
+ createAppResourceReadHandler,
2193
+ foldArmFindings,
2194
+ liveLinkLine,
2195
+ liveLinkLineFor,
2196
+ liveRunLinkOf,
2197
+ maskedToolResult,
2198
+ negotiatedAppsExtension,
2199
+ parseAppUri,
2200
+ projectJobAsTask,
2201
+ registerAppResources,
2202
+ renderAppWrapperPage,
2203
+ stripRedundantObjectStamp,
2204
+ toRegisterToolConfig,
2205
+ withoutDialect,
2206
+ withoutLegacyResultWrap
1140
2207
  });
1141
2208
  //# sourceMappingURL=index.cjs.map