common-qa-mcp 0.1.0

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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/adapters/factory.d.ts +15 -0
  4. package/dist/adapters/factory.d.ts.map +1 -0
  5. package/dist/adapters/factory.js +18 -0
  6. package/dist/adapters/factory.js.map +1 -0
  7. package/dist/adapters/remoteMcp/adapter.d.ts +92 -0
  8. package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -0
  9. package/dist/adapters/remoteMcp/adapter.js +219 -0
  10. package/dist/adapters/remoteMcp/adapter.js.map +1 -0
  11. package/dist/adapters/types.d.ts +77 -0
  12. package/dist/adapters/types.d.ts.map +1 -0
  13. package/dist/adapters/types.js +2 -0
  14. package/dist/adapters/types.js.map +1 -0
  15. package/dist/adapters/walnut/actionTypes.d.ts +16 -0
  16. package/dist/adapters/walnut/actionTypes.d.ts.map +1 -0
  17. package/dist/adapters/walnut/actionTypes.js +141 -0
  18. package/dist/adapters/walnut/actionTypes.js.map +1 -0
  19. package/dist/adapters/walnut/mapper.d.ts +34 -0
  20. package/dist/adapters/walnut/mapper.d.ts.map +1 -0
  21. package/dist/adapters/walnut/mapper.js +228 -0
  22. package/dist/adapters/walnut/mapper.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +36 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config.d.ts +81 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +211 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/connection.d.ts +101 -0
  32. package/dist/connection.d.ts.map +1 -0
  33. package/dist/connection.js +190 -0
  34. package/dist/connection.js.map +1 -0
  35. package/dist/domain/types.d.ts +279 -0
  36. package/dist/domain/types.d.ts.map +1 -0
  37. package/dist/domain/types.js +2 -0
  38. package/dist/domain/types.js.map +1 -0
  39. package/dist/domain/validate.d.ts +3 -0
  40. package/dist/domain/validate.d.ts.map +1 -0
  41. package/dist/domain/validate.js +29 -0
  42. package/dist/domain/validate.js.map +1 -0
  43. package/dist/drafts/store.d.ts +62 -0
  44. package/dist/drafts/store.d.ts.map +1 -0
  45. package/dist/drafts/store.js +119 -0
  46. package/dist/drafts/store.js.map +1 -0
  47. package/dist/httpMain.d.ts +3 -0
  48. package/dist/httpMain.d.ts.map +1 -0
  49. package/dist/httpMain.js +22 -0
  50. package/dist/httpMain.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +46 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/initAgents.d.ts +2 -0
  56. package/dist/initAgents.d.ts.map +1 -0
  57. package/dist/initAgents.js +178 -0
  58. package/dist/initAgents.js.map +1 -0
  59. package/dist/log.d.ts +14 -0
  60. package/dist/log.d.ts.map +1 -0
  61. package/dist/log.js +14 -0
  62. package/dist/log.js.map +1 -0
  63. package/dist/mcpResult.d.ts +20 -0
  64. package/dist/mcpResult.d.ts.map +1 -0
  65. package/dist/mcpResult.js +8 -0
  66. package/dist/mcpResult.js.map +1 -0
  67. package/dist/mount.d.ts +12 -0
  68. package/dist/mount.d.ts.map +1 -0
  69. package/dist/mount.js +126 -0
  70. package/dist/mount.js.map +1 -0
  71. package/dist/projectStore.d.ts +5 -0
  72. package/dist/projectStore.d.ts.map +1 -0
  73. package/dist/projectStore.js +60 -0
  74. package/dist/projectStore.js.map +1 -0
  75. package/dist/requestConnection.d.ts +20 -0
  76. package/dist/requestConnection.d.ts.map +1 -0
  77. package/dist/requestConnection.js +44 -0
  78. package/dist/requestConnection.js.map +1 -0
  79. package/dist/serve.d.ts +10 -0
  80. package/dist/serve.d.ts.map +1 -0
  81. package/dist/serve.js +37 -0
  82. package/dist/serve.js.map +1 -0
  83. package/dist/server.d.ts +29 -0
  84. package/dist/server.d.ts.map +1 -0
  85. package/dist/server.js +76 -0
  86. package/dist/server.js.map +1 -0
  87. package/dist/tools/connection.tools.d.ts +16 -0
  88. package/dist/tools/connection.tools.d.ts.map +1 -0
  89. package/dist/tools/connection.tools.js +106 -0
  90. package/dist/tools/connection.tools.js.map +1 -0
  91. package/dist/tools/discovery.tools.d.ts +16 -0
  92. package/dist/tools/discovery.tools.d.ts.map +1 -0
  93. package/dist/tools/discovery.tools.js +369 -0
  94. package/dist/tools/discovery.tools.js.map +1 -0
  95. package/dist/tools/markdown.d.ts +13 -0
  96. package/dist/tools/markdown.d.ts.map +1 -0
  97. package/dist/tools/markdown.js +65 -0
  98. package/dist/tools/markdown.js.map +1 -0
  99. package/dist/tools/recording.tools.d.ts +16 -0
  100. package/dist/tools/recording.tools.d.ts.map +1 -0
  101. package/dist/tools/recording.tools.js +378 -0
  102. package/dist/tools/recording.tools.js.map +1 -0
  103. package/dist/tools/run.tools.d.ts +13 -0
  104. package/dist/tools/run.tools.d.ts.map +1 -0
  105. package/dist/tools/run.tools.js +230 -0
  106. package/dist/tools/run.tools.js.map +1 -0
  107. package/dist/tools/schemas.d.ts +560 -0
  108. package/dist/tools/schemas.d.ts.map +1 -0
  109. package/dist/tools/schemas.js +112 -0
  110. package/dist/tools/schemas.js.map +1 -0
  111. package/dist/tools/testcase.tools.d.ts +50 -0
  112. package/dist/tools/testcase.tools.d.ts.map +1 -0
  113. package/dist/tools/testcase.tools.js +384 -0
  114. package/dist/tools/testcase.tools.js.map +1 -0
  115. package/dist/vendor/actionSignatures.d.ts +117 -0
  116. package/dist/vendor/actionSignatures.d.ts.map +1 -0
  117. package/dist/vendor/actionSignatures.js +420 -0
  118. package/dist/vendor/actionSignatures.js.map +1 -0
  119. package/dist/vendor/logger.d.ts +38 -0
  120. package/dist/vendor/logger.d.ts.map +1 -0
  121. package/dist/vendor/logger.js +174 -0
  122. package/dist/vendor/logger.js.map +1 -0
  123. package/package.json +57 -0
@@ -0,0 +1,378 @@
1
+ import { z } from 'zod';
2
+ import { ok, err } from '../mcpResult.js';
3
+ import { log } from '../log.js';
4
+ /**
5
+ * Bridges the product MCP's AGENT-BOUND recording surface (recording_start, browser_*, verify_*,
6
+ * control_flow_marker, generate_test_steps, recording_steps/save/discard, ...) through the Common
7
+ * MCP, so the client drives recording through the ONE door instead of connecting to the agent
8
+ * directly. Common cannot implement these (they need the product's real browser); it FORWARDS each
9
+ * call verbatim via `adapter.callRaw` and relays the result.
10
+ *
11
+ * The set is mirrored DYNAMICALLY from the product's own advertised tools (real names + descriptions,
12
+ * so it never drifts), minus the provider-ops Common already wraps as its own client tools. It runs
13
+ * async once the connection is live; registerTool emits tools/list_changed so the client picks the
14
+ * proxies up (a /mcp reconnect also surfaces them).
15
+ *
16
+ * DELIBERATELY ROUGH (fix later, per plan): input schemas are permissive (the agent validates
17
+ * strictly) rather than mirrored exactly; and recording_save creates a draft in the AGENT's store,
18
+ * which Common's tc_save cannot see -- persisting a recorded test still needs a draft->tc_save bridge.
19
+ */
20
+ /** Agent provider-ops Common already exposes as its own client tools -- NOT re-proxied. */
21
+ const PROVIDER_OPS = new Set([
22
+ 'info', 'list_projects', 'list_testcases', 'get_testcase', 'actions_catalog',
23
+ 'create_testcase', 'update_testcase', 'list_cloud_runners', 'start_run', 'get_run', 'stop_run',
24
+ ]);
25
+ /**
26
+ * The ONLY agent tools re-exposed to the client: the technology SUB-AGENT tools. Each is a DELEGATED
27
+ * sub-agent (goal in; its own LLM loop over internal browser + recording tools; a saved draft out).
28
+ * The granular tools (browser_*, recording_*, verify_*, store_run_variable, control_flow_marker,
29
+ * generate_test_steps, qa_select_technology, qa_technology_status) stay INTERNAL to the sub-agents on
30
+ * the product agent and are NOT proxied -- the client drives everything through web / android /
31
+ * mainframe / db plus the lifecycle provider-ops. This is the "one door, smartness downstream" surface.
32
+ */
33
+ const TECHNOLOGY_SUBAGENTS = new Set(['web', 'android', 'mainframe', 'db']);
34
+ /**
35
+ * The POLL half of a delegated recording. A delegated recorder is non-blocking — `web`/`mainframe`
36
+ * return a sessionId and record in the background — so its `*_status` tool is as much part of the
37
+ * client surface as the recorder itself, and each one needs the DRAFT BRIDGE (see
38
+ * registerDelegatedStatus). Without its status tool a technology can START a recording that can never
39
+ * be collected: the assembled draft stays in the AGENT's store, which Common's tc_save cannot see.
40
+ *
41
+ * Keep this in lockstep with the recorders in TECHNOLOGY_SUBAGENTS that are non-blocking. `android`
42
+ * and `db` are absent because they are still declared scaffolds with no delegated loop to poll.
43
+ */
44
+ const DELEGATED_STATUS_TOOLS = new Set(['web_status', 'mainframe_status']);
45
+ /** Permissive input: accept any object; the product MCP is the strict validator. */
46
+ const PASSTHROUGH_INPUT = z.record(z.string(), z.unknown());
47
+ /**
48
+ * Per-technology wording + extra input fields for the two delegated recorders. Everything else about
49
+ * them is identical (resolve nested refs up front, forward, poll, re-home the draft), which is why
50
+ * they share registerDelegatedRecorder / registerDelegatedStatus rather than each carrying a copy.
51
+ */
52
+ const DELEGATED_RECORDERS = {
53
+ web: {
54
+ statusTool: 'web_status',
55
+ /** Extra fields beyond `task` + nestStart/nestEnd, forwarded verbatim to the agent. */
56
+ extraInput: {
57
+ url: z.string().optional().describe('Starting URL for the recording browser. STRONGLY recommended — the recording opens on this ' +
58
+ 'page. If omitted, the agent uses the first URL found in the task.'),
59
+ profile: z.string().optional().describe('Persistent browser profile (cookies/localStorage). Omit for a fresh isolated profile per ' +
60
+ 'recording (recommended — avoids browser-lock collisions); pass a name to REUSE a signed-in ' +
61
+ 'profile across recordings.'),
62
+ },
63
+ taskDescribe: 'The GOAL in natural language, e.g. "Sign in at <url> as Admin/admin123, then verify the ' +
64
+ 'Dashboard heading is visible." Include the app URL in the task.',
65
+ startNote: 'NON-BLOCKING: this STARTS the delegated web recording on the agent and returns a sessionId ' +
66
+ 'immediately — a browser opens and records in the background. Then poll web_status({ sessionId }) ' +
67
+ 'until status is "done"; web_status re-homes the draft HERE and returns a Common draftId that ' +
68
+ 'tc_save can persist. Call web ONCE — do not retry it.',
69
+ doneLabel: 'Web recording complete.',
70
+ },
71
+ mainframe: {
72
+ statusTool: 'mainframe_status',
73
+ extraInput: {
74
+ appPath: z.string().optional().describe('Path to the TN3270 emulator SESSION FILE or its executable. The extension identifies the ' +
75
+ 'emulator: `.ecf` = Quick3270, `.ws` = IBM Personal Communications, `.rd3x`/`.rd5x` = Micro ' +
76
+ 'Focus Reflection. Launched before recording and becomes the "Launch ${app_path}" step 1, so ' +
77
+ 'the test case replays from cold. Omit ONLY when the emulator is already open and you are ' +
78
+ 'attaching to its live session. If you do not know the path, ASK THE USER for it — do not ' +
79
+ 'search the filesystem.'),
80
+ attachOnly: z.boolean().optional().describe('Do not launch the emulator even though appPath was given — attach to the running one.'),
81
+ },
82
+ taskDescribe: 'The GOAL in natural language, e.g. "Log on to TSO as ANISHA/pass123, then run LISTUSER." ' +
83
+ 'Include the real values the host needs — they are parameterized at assembly time.',
84
+ startNote: 'NON-BLOCKING: this STARTS the delegated mainframe recording on the agent and returns a sessionId ' +
85
+ 'immediately — the emulator opens and records in the background. Then poll ' +
86
+ 'mainframe_status({ sessionId }) until status is "done"; mainframe_status re-homes the draft HERE ' +
87
+ 'and returns a Common draftId that tc_save can persist. Call mainframe ONCE — do not retry it: ' +
88
+ 'the HLLAPI session admits one attachment, so mainframe recordings cannot overlap.',
89
+ doneLabel: 'Mainframe recording complete.',
90
+ },
91
+ };
92
+ /**
93
+ * Convert a product tool's JSON-Schema (object schema) into a Zod raw shape, so the PROXIED tool
94
+ * advertises the SAME named parameters (ref/expected/value/count/…) and required-ness to the client
95
+ * instead of an opaque permissive blob. Without this the client's model can't tell that, say,
96
+ * web_validate_text REQUIRES `expected`; it sends nothing, and the agent invokes a 1-arg method with
97
+ * 0 args ("Arguments length does not match"). Types are approximated (the product agent is the strict
98
+ * validator); the point is that the model knows WHICH args to send. Returns null when the schema is
99
+ * absent or not an object schema, so the caller falls back to the permissive record.
100
+ */
101
+ function jsonSchemaToShape(schema) {
102
+ if (!schema || typeof schema !== 'object')
103
+ return null;
104
+ const s = schema;
105
+ if (s.type !== 'object' || !s.properties || typeof s.properties !== 'object')
106
+ return null;
107
+ const required = new Set(Array.isArray(s.required) ? s.required.map(String) : []);
108
+ const shape = {};
109
+ for (const [key, raw] of Object.entries(s.properties)) {
110
+ const p = (raw && typeof raw === 'object' ? raw : {});
111
+ let field;
112
+ switch (p.type) {
113
+ case 'string':
114
+ field = z.string();
115
+ break;
116
+ case 'number':
117
+ field = z.number();
118
+ break;
119
+ case 'integer':
120
+ field = z.number().int();
121
+ break;
122
+ case 'boolean':
123
+ field = z.boolean();
124
+ break;
125
+ case 'array':
126
+ field = z.array(z.unknown());
127
+ break;
128
+ case 'object':
129
+ field = z.record(z.string(), z.unknown());
130
+ break;
131
+ default: field = z.unknown();
132
+ }
133
+ if (typeof p.description === 'string')
134
+ field = field.describe(p.description);
135
+ if (!required.has(key))
136
+ field = field.optional();
137
+ shape[key] = field;
138
+ }
139
+ return Object.keys(shape).length ? shape : null;
140
+ }
141
+ export function registerRecordingPassthrough(server, deps) {
142
+ void (async () => {
143
+ const adapter = await waitForConnectedAdapter(deps, 12000);
144
+ if (!adapter?.listAgentTools || !adapter.callRaw)
145
+ return; // offline, or a product without passthrough
146
+ // Retry listAgentTools until the downstream product MCP is primed and advertising. At startup the
147
+ // Common connection can report "connected" (config present) BEFORE the product MCP's OWN connection
148
+ // is primed, so a single call races it, returns nothing, and the web/android/... tools never appear
149
+ // for the whole session (observed after a client reload). The first attempt also nudges the prime.
150
+ // Poll until we get a non-empty list, or give up after ~30s.
151
+ let tools = [];
152
+ const deadline = Date.now() + 30000;
153
+ while (Date.now() < deadline) {
154
+ try {
155
+ const a = deps.getAdapter();
156
+ if (a?.listAgentTools) {
157
+ tools = await a.listAgentTools();
158
+ if (tools.length)
159
+ break;
160
+ }
161
+ }
162
+ catch { /* downstream not primed yet — retry */ }
163
+ await new Promise((r) => setTimeout(r, 500));
164
+ }
165
+ if (!tools.length) {
166
+ log('recording passthrough: no agent tools discovered (product MCP not ready within 30s)');
167
+ return;
168
+ }
169
+ // SimplifyQA-only behavior below (granular-verb proxying + real-schema preservation). Walnut keeps
170
+ // its EXACT prior surface, so this cannot affect Walnut. Evaluated once the connection is live.
171
+ const simplifyqa = deps.isSimplifyqa();
172
+ let proxied = 0;
173
+ for (const t of tools) {
174
+ if (PROVIDER_OPS.has(t.name))
175
+ continue;
176
+ // Two recording surfaces are proxied:
177
+ // (a) WALNUT — the technology SUB-AGENT tools (web/android/mainframe/db) + `qa_technology_status`
178
+ // (which LISTS the sub-agents + their availability, the way list_projects lists projects) +
179
+ // every non-blocking recorder's own *_status tool. One delegated sub-agent per tech; its
180
+ // granular browser_*/recording_*/verify_* tools stay INTERNAL to the agent's own loop.
181
+ // The status tools come from DELEGATED_STATUS_TOOLS, NOT a hardcoded 'web_status':
182
+ // `mainframe_status` is equally a client-surface tool, and it matches no core_*/web_* prefix,
183
+ // so naming only web_status here would leave a started mainframe recording uncollectable.
184
+ // (b) SIMPLIFYQA — the recorder is driven by the CLIENT's own LLM through GRANULAR verbs
185
+ // (core_activate_technology, web_launch_application, web_snapshot, web_click, core_if,
186
+ // core_loop, web_switch_*, …). Those ARE the interface here, so proxy every core_*/web_* verb
187
+ // verbatim (the generic forward branch below) — SimplifyQA ONLY. Prefixes don't collide:
188
+ // Walnut's single tool is exactly `web` (no underscore) and its internals use browser_/…
189
+ const isSubAgent = TECHNOLOGY_SUBAGENTS.has(t.name)
190
+ || t.name === 'qa_technology_status'
191
+ || DELEGATED_STATUS_TOOLS.has(t.name);
192
+ const isRecorderVerb = simplifyqa && (t.name.startsWith('core_') || t.name.startsWith('web_'));
193
+ if (!isSubAgent && !isRecorderVerb)
194
+ continue;
195
+ try {
196
+ // A delegated recorder + its status tool get the DRAFT BRIDGE, not a dumb forward: the
197
+ // recording assembles a test case into a draft in the AGENT's OWN store, which tc_save
198
+ // (Common's store) can't see. So we forward, then re-register the returned TestCase in
199
+ // Common's DraftStore and hand back a COMMON draftId — the only kind tc_save can persist.
200
+ const recorderCfg = DELEGATED_RECORDERS[t.name];
201
+ if (recorderCfg) {
202
+ registerDelegatedRecorder(server, deps, t, recorderCfg);
203
+ proxied++;
204
+ continue;
205
+ }
206
+ const statusOwner = Object.values(DELEGATED_RECORDERS).find((c) => c.statusTool === t.name);
207
+ if (statusOwner) {
208
+ registerDelegatedStatus(server, deps, t, statusOwner);
209
+ proxied++;
210
+ continue;
211
+ }
212
+ // SimplifyQA: preserve the agent's REAL parameter schema (ref/expected/value/count/…) so the
213
+ // client sends the right args (without it, web_validate_text's required `expected` is hidden →
214
+ // "Arguments length does not match"). Walnut keeps the permissive record exactly as before.
215
+ const shape = simplifyqa ? jsonSchemaToShape(t.inputSchema) : null;
216
+ server.registerTool(t.name, {
217
+ description: `${t.description ?? t.name}\n\n(Agent-bound tool — runs on the connected product agent, forwarded through the Common MCP.)`,
218
+ inputSchema: shape ?? PASSTHROUGH_INPUT,
219
+ }, async (args = {}) => {
220
+ const a = deps.getAdapter();
221
+ if (!a?.callRaw)
222
+ return err('Not connected to a product agent — recording runs on the agent (qa_connect first).');
223
+ try {
224
+ return (await a.callRaw(t.name, args));
225
+ }
226
+ catch (e) {
227
+ return err(e instanceof Error ? e.message : String(e));
228
+ }
229
+ });
230
+ proxied++;
231
+ }
232
+ catch {
233
+ // Name already registered (a Common-native tool) -- skip rather than clobber.
234
+ }
235
+ }
236
+ log(`recording passthrough: proxied ${proxied} agent tool(s)`);
237
+ })();
238
+ }
239
+ /**
240
+ * Register a DELEGATED RECORDER (`web`, `mainframe`) with a REAL schema rather than the permissive
241
+ * passthrough: `task` is required, and the technology's own extra fields are surfaced so the caller is
242
+ * nudged to pass them (a web recording wants `url` so it starts on the real page instead of
243
+ * about:blank; a mainframe recording wants `appPath` so the emulator gets launched and step 1 is
244
+ * recorded).
245
+ *
246
+ * `nestStart`/`nestEnd` arrive here as test-case IDS but go DOWN as resolved `{ tcId, name }` refs —
247
+ * resolved up front so a bad id fails BEFORE a recording is wasted, and so the agent can compose the
248
+ * nesting INTO the session (making the live editor show, and its Save persist, the whole composition).
249
+ */
250
+ function registerDelegatedRecorder(server, deps, t, cfg) {
251
+ server.registerTool(t.name, {
252
+ description: `${t.description ?? t.name}\n\n(${cfg.startNote})`,
253
+ inputSchema: {
254
+ task: z.string().min(1).describe(cfg.taskDescribe),
255
+ ...cfg.extraInput,
256
+ nestStart: z.array(z.string()).optional().describe('Existing test case ids to run BEFORE the recorded activity, as nested steps (e.g. a Login ' +
257
+ 'test case). They are prepended to the produced draft in order — so the draft becomes ' +
258
+ '[nestStart…] → recorded activity → [nestEnd…]. Use this to reuse a login flow instead of ' +
259
+ 're-recording it.'),
260
+ nestEnd: z.array(z.string()).optional().describe('Existing test case ids to run AFTER the recorded activity, as nested steps (e.g. a Logout ' +
261
+ 'test case). They are appended to the produced draft in order.'),
262
+ },
263
+ },
264
+ // `args` is annotated rather than inferred: the schema above SPREADS cfg.extraInput, whose type is
265
+ // a union across the technologies, so the SDK cannot infer a single concrete arg shape (implicit
266
+ // any). Reading fields off a Record keeps this honest — the agent is the strict validator anyway.
267
+ async (args) => {
268
+ const a = deps.getAdapter();
269
+ if (!a?.callRaw)
270
+ return err('Not connected to a product agent — recording runs on the agent (qa_connect first).');
271
+ const rec = args ?? {};
272
+ const idsOf = (key) => Array.isArray(rec[key]) ? rec[key].map(String) : [];
273
+ let startRefs;
274
+ let endRefs;
275
+ try {
276
+ startRefs = await resolveNestedRefs(a, idsOf('nestStart'));
277
+ endRefs = await resolveNestedRefs(a, idsOf('nestEnd'));
278
+ }
279
+ catch (e) {
280
+ return err(e instanceof Error ? e.message : String(e));
281
+ }
282
+ // Forward `task` plus whichever of this technology's own fields the caller actually supplied.
283
+ // Driven off cfg.extraInput's keys so adding a field to the config is the only edit needed.
284
+ const agentArgs = { task: rec['task'] };
285
+ for (const key of Object.keys(cfg.extraInput)) {
286
+ if (rec[key] !== undefined)
287
+ agentArgs[key] = rec[key];
288
+ }
289
+ if (startRefs.length)
290
+ agentArgs['nestStart'] = startRefs;
291
+ if (endRefs.length)
292
+ agentArgs['nestEnd'] = endRefs;
293
+ // NON-BLOCKING: the agent returns a sessionId immediately (recording runs in the background), so
294
+ // this returns in ms — no long-blocking RPC, no client-timeout-driven retries, no duplicate
295
+ // browser/emulator. The draft is re-homed later, on the status tool's "done".
296
+ try {
297
+ return (await a.callRaw(t.name, agentArgs));
298
+ }
299
+ catch (e) {
300
+ return err(e instanceof Error ? e.message : String(e));
301
+ }
302
+ });
303
+ }
304
+ /**
305
+ * Register a delegated recorder's POLL tool (`web_status`, `mainframe_status`) — and with it THE DRAFT
306
+ * BRIDGE. While the recording is running the downstream result is forwarded verbatim; on "done" the
307
+ * agent's assembled TestCase is re-registered in COMMON's DraftStore, because that is the only store
308
+ * `tc_save` can read. Without this hop a finished recording would be visible but unsaveable.
309
+ */
310
+ function registerDelegatedStatus(server, deps, t, cfg) {
311
+ const starter = t.name.replace(/_status$/, '');
312
+ server.registerTool(t.name, {
313
+ description: `${t.description ?? t.name}\n\n(Poll a recording started by ${starter}(). While it reports ` +
314
+ `"recording", poll again. On "done" this re-homes the assembled draft into Common's store and ` +
315
+ `returns a Common draftId — review the steps, then tc_save({ draftId }) to persist.)`,
316
+ inputSchema: { sessionId: z.string().describe(`The sessionId returned by ${starter}().`) },
317
+ annotations: { readOnlyHint: true },
318
+ }, async (args) => {
319
+ const a = deps.getAdapter();
320
+ if (!a?.callRaw)
321
+ return err('Not connected to a product agent — recording runs on the agent (qa_connect first).');
322
+ let res;
323
+ try {
324
+ res = (await a.callRaw(t.name, { sessionId: args.sessionId }));
325
+ }
326
+ catch (e) {
327
+ return err(e instanceof Error ? e.message : String(e));
328
+ }
329
+ if (res.isError)
330
+ return res; // recording error — forward verbatim
331
+ const sc = res.structuredContent;
332
+ if (sc?.status !== 'done' || !sc.tc)
333
+ return res; // still recording → forward as-is
334
+ const draftId = deps.drafts.create(sc.tc);
335
+ const stored = deps.drafts.get(draftId).tc;
336
+ const stepCount = Array.isArray(stored.steps) ? stored.steps.length : (sc.stepCount ?? 0);
337
+ return ok(`${sc.summary ?? cfg.doneLabel}\n\nRegistered as draft ${draftId} ` +
338
+ `(${stepCount} step${stepCount === 1 ? '' : 's'}). Persist it with ` +
339
+ `tc_save({ draftId: "${draftId}" }) — that saves it to Walnut and opens it in the Walnut UI.`, { status: 'done', draftId, tc: stored, stepCount, ...(sc.summary ? { summary: sc.summary } : {}) });
340
+ });
341
+ }
342
+ /**
343
+ * Resolve existing test-case ids to { tcId, name } refs for a delegated recorder to compose as nested
344
+ * steps DURING recording. Titles come from getTestCase, so a wrong id fails BEFORE a recording is
345
+ * wasted; the id is passed through as-is (function_call_data.test_case_id is a String — a business key
346
+ * TC_123 or a Mongo _id both persist). The agent builds the actual function steps in-session so the
347
+ * live editor renders + saves them.
348
+ */
349
+ async function resolveNestedRefs(adapter, tcIds) {
350
+ const refs = [];
351
+ for (const id of tcIds) {
352
+ let name = id;
353
+ try {
354
+ const child = await adapter.getTestCase(id);
355
+ if (child?.title)
356
+ name = child.title;
357
+ }
358
+ catch {
359
+ throw new Error(`Cannot reuse test case "${id}" as a nested step — it was not found (check the id).`);
360
+ }
361
+ refs.push({ tcId: id, name });
362
+ }
363
+ return refs;
364
+ }
365
+ /** Poll until the shared connection is live and its adapter can enumerate the agent's tools. */
366
+ async function waitForConnectedAdapter(deps, timeoutMs) {
367
+ const start = Date.now();
368
+ while (Date.now() - start < timeoutMs) {
369
+ if (deps.isConnected()) {
370
+ const a = deps.getAdapter();
371
+ if (a?.listAgentTools)
372
+ return a;
373
+ }
374
+ await new Promise((r) => setTimeout(r, 250));
375
+ }
376
+ return deps.isConnected() ? deps.getAdapter() : null;
377
+ }
378
+ //# sourceMappingURL=recording.tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recording.tools.js","sourceRoot":"","sources":["../../src/tools/recording.tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ProviderAdapter } from '../adapters/types.js';
3
+ /**
4
+ * The client-facing RUN surface: cloud_runners / tc_run / run_status / run_stop. Like the discovery
5
+ * tools these need a LIVE backend, so they read the current provider adapter per call via
6
+ * `deps.getAdapter()` and forward across the product hop. tc_run owns the local-vs-cloud DECISION
7
+ * policy (ask when unstated; refuse cloud when no runner is online) so every client behaves the same.
8
+ */
9
+ export type RunToolsDeps = {
10
+ getAdapter: () => ProviderAdapter | null;
11
+ };
12
+ export declare function registerRunTools(server: McpServer, deps: RunToolsDeps): void;
13
+ //# sourceMappingURL=run.tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.tools.d.ts","sourceRoot":"","sources":["../../src/tools/run.tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CAC1C,CAAC;AA0BF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA8O5E"}
@@ -0,0 +1,230 @@
1
+ import { z } from 'zod';
2
+ import { ok, err } from '../mcpResult.js';
3
+ const NOT_CONNECTED = 'Not connected to a product. Use qa_connect({ url, token, projectId }) first — running a test needs a live backend connection.';
4
+ function describeError(e) {
5
+ return e instanceof Error ? e.message : String(e);
6
+ }
7
+ function runnerLine(r) {
8
+ return `- ${r.online ? '🟢' : '⚪'} ${r.name} \`${r.id}\`${r.status ? ` (${r.status})` : ''}`;
9
+ }
10
+ /** Human summary of a run's status. For a SUITE run `children` carries the per-test-case rollup and
11
+ * `progress` counts TEST CASES; a single test-case run has neither (progress counts steps). */
12
+ function statusText(s) {
13
+ const unit = s.children ? 'test cases' : 'steps';
14
+ const prog = s.progress ? ` · ${s.progress.done}/${s.progress.total} ${unit}` : '';
15
+ const link = s.reportUrl ? `\n${s.reportUrl}` : '';
16
+ const icon = (state) => (state === 'passed' ? '✅' : state === 'failed' ? '❌' : state === 'running' ? '⏳' : '•');
17
+ const kids = s.children?.length
18
+ ? '\n\n' + s.children.map((c) => `- ${icon(c.state)} ${c.id}${c.title ? ` — ${c.title}` : ''} (${c.state})`).join('\n')
19
+ : '';
20
+ return `Run ${s.runId} · **${s.state}**${prog}${link}${kids}`;
21
+ }
22
+ export function registerRunTools(server, deps) {
23
+ server.registerTool('cloud_runners', {
24
+ title: 'List cloud runners (and which are online)',
25
+ description: 'List the cloud runner VMs registered to the connected project and which are online. Use this ' +
26
+ 'to show the user the cloud options, or to pick a specific runner id for tc_run. Requires a live connection.',
27
+ inputSchema: {},
28
+ annotations: { readOnlyHint: true },
29
+ }, async () => {
30
+ const adapter = deps.getAdapter();
31
+ if (!adapter)
32
+ return err(NOT_CONNECTED);
33
+ if (!adapter.listCloudRunners)
34
+ return err('This product has no cloud runners.');
35
+ try {
36
+ const runners = await adapter.listCloudRunners();
37
+ const online = runners.filter((r) => r.online);
38
+ if (!runners.length)
39
+ return ok('No cloud runners are registered to this project.', { runners: [] });
40
+ const text = `${runners.length} cloud runner${runners.length === 1 ? '' : 's'} · ${online.length} online:\n\n` +
41
+ runners.map(runnerLine).join('\n');
42
+ return ok(text, { runners, online: online.length });
43
+ }
44
+ catch (e) {
45
+ return err(describeError(e));
46
+ }
47
+ });
48
+ server.registerTool('tc_run', {
49
+ title: 'Run a test case (local or cloud)',
50
+ description: 'Execute ONE existing test case in the connected product. `mode` decides WHERE it runs:\n' +
51
+ " • 'local' — the connected agent's own browser\n" +
52
+ " • 'cloud' — an online cloud runner VM\n" +
53
+ 'IF THE USER HAS NOT SAID local or cloud, ASK THEM before calling — do not guess. When mode is ' +
54
+ 'omitted this tool refuses with that instruction. For cloud, if no runner is online it reports that ' +
55
+ 'there are no active cloud runners (nothing is run). Returns the run id — poll run_status for the result.',
56
+ inputSchema: {
57
+ tcId: z.string().describe('The test case id (or business key) to run.'),
58
+ mode: z.enum(['local', 'cloud']).optional().describe("Where to run. REQUIRED in practice — ask the user if unstated."),
59
+ environment: z.string().optional().describe('Environment id (Mongo id); a name is ignored and the project default is used.'),
60
+ browser: z.string().optional().describe("Browser, default 'chromium' (bundled + isolated). Others: 'chrome'/'msedge' (installed channel), 'firefox', 'webkit'."),
61
+ headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false.'),
62
+ cloudRunnerId: z.string().optional().describe('For cloud: a specific runner id (from cloud_runners). Omit to auto-pick an online one.'),
63
+ },
64
+ annotations: { readOnlyHint: false },
65
+ }, async ({ tcId, mode, environment, browser, headless, cloudRunnerId }) => {
66
+ const adapter = deps.getAdapter();
67
+ if (!adapter)
68
+ return err(NOT_CONNECTED);
69
+ if (!mode) {
70
+ return err("Specify where to run: call tc_run again with mode: 'local' or 'cloud'. " +
71
+ 'If the user has not said which, ask them first — local runs on the connected agent, ' +
72
+ 'cloud runs on an online cloud runner VM.');
73
+ }
74
+ let resolvedRunner;
75
+ if (mode === 'cloud') {
76
+ if (!adapter.listCloudRunners)
77
+ return err('This product has no cloud runners; run with mode: "local".');
78
+ let runners;
79
+ try {
80
+ runners = await adapter.listCloudRunners();
81
+ }
82
+ catch (e) {
83
+ return err(describeError(e));
84
+ }
85
+ const online = runners.filter((r) => r.online);
86
+ if (!online.length) {
87
+ return err('Currently there are no active cloud runners in your project.');
88
+ }
89
+ if (cloudRunnerId) {
90
+ resolvedRunner = online.find((r) => r.id === cloudRunnerId);
91
+ if (!resolvedRunner) {
92
+ return err(`Cloud runner "${cloudRunnerId}" is not online. Online now:\n` + online.map(runnerLine).join('\n'));
93
+ }
94
+ }
95
+ else {
96
+ resolvedRunner = online[0]; // auto-pick the first online runner
97
+ }
98
+ }
99
+ const opts = {
100
+ mode,
101
+ ...(environment !== undefined ? { environment } : {}),
102
+ ...(browser !== undefined ? { browser } : {}),
103
+ ...(headless !== undefined ? { headless } : {}),
104
+ ...(resolvedRunner ? { cloudRunnerId: resolvedRunner.id } : {}),
105
+ };
106
+ try {
107
+ const run = await adapter.startRun({ tcId }, opts);
108
+ const where = mode === 'cloud'
109
+ ? `on cloud runner ${resolvedRunner ? `**${resolvedRunner.name}**` : ''}`
110
+ : 'locally on the connected agent';
111
+ return ok(`Started ${tcId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
112
+ }
113
+ catch (e) {
114
+ return err(describeError(e));
115
+ }
116
+ });
117
+ server.registerTool('suite_run', {
118
+ title: 'Run a test suite (local or cloud)',
119
+ description: 'Execute an existing test SUITE — all its member test cases, in order — in the connected product. ' +
120
+ '`mode` decides WHERE it runs:\n' +
121
+ " • 'local' — the connected agent's own browser (always serial)\n" +
122
+ " • 'cloud' — online cloud runner VM(s)\n" +
123
+ 'IF THE USER HAS NOT SAID local or cloud, ASK THEM before calling — do not guess (this tool refuses ' +
124
+ 'when mode is omitted). For cloud, if no runner is online it reports that there are no active cloud ' +
125
+ 'runners (nothing is run). Returns the suite-run id — poll run_status for progress (it reports ' +
126
+ 'per-test-case results under `children`) and run_stop to cancel.',
127
+ inputSchema: {
128
+ suiteId: z.string().describe('The suite id to run (from suite_list / suite_get).'),
129
+ mode: z.enum(['local', 'cloud']).optional().describe('Where to run. REQUIRED in practice — ask the user if unstated.'),
130
+ environment: z.string().optional().describe('Environment id (Mongo id); a name is ignored and the project default is used.'),
131
+ browser: z.string().optional().describe("Browser, default 'chromium'. Others: 'chrome'/'msedge', 'firefox', 'webkit'."),
132
+ headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false.'),
133
+ cloudRunnerId: z.string().optional().describe('For cloud: a specific runner id (from cloud_runners). Omit to auto-pick an online one.'),
134
+ strategy: z.enum(['serial', 'batch', 'dynamic']).optional().describe('Cloud dispatch order across the suite. Default serial; local is always serial.'),
135
+ maxConcurrent: z.number().int().positive().optional().describe('Cloud batch/dynamic only — max test cases running at once.'),
136
+ terminateOnFailure: z.boolean().optional().describe('Stop dispatching remaining test cases after the first failure.'),
137
+ },
138
+ annotations: { readOnlyHint: false },
139
+ }, async ({ suiteId, mode, environment, browser, headless, cloudRunnerId, strategy, maxConcurrent, terminateOnFailure }) => {
140
+ const adapter = deps.getAdapter();
141
+ if (!adapter)
142
+ return err(NOT_CONNECTED);
143
+ if (!mode) {
144
+ return err("Specify where to run: call suite_run again with mode: 'local' or 'cloud'. " +
145
+ 'If the user has not said which, ask them first — local runs on the connected agent, ' +
146
+ 'cloud runs on online cloud runner VM(s).');
147
+ }
148
+ let resolvedRunner;
149
+ if (mode === 'cloud') {
150
+ if (!adapter.listCloudRunners)
151
+ return err('This product has no cloud runners; run with mode: "local".');
152
+ let runners;
153
+ try {
154
+ runners = await adapter.listCloudRunners();
155
+ }
156
+ catch (e) {
157
+ return err(describeError(e));
158
+ }
159
+ const online = runners.filter((r) => r.online);
160
+ if (!online.length) {
161
+ return err('Currently there are no active cloud runners in your project.');
162
+ }
163
+ if (cloudRunnerId) {
164
+ resolvedRunner = online.find((r) => r.id === cloudRunnerId);
165
+ if (!resolvedRunner) {
166
+ return err(`Cloud runner "${cloudRunnerId}" is not online. Online now:\n` + online.map(runnerLine).join('\n'));
167
+ }
168
+ }
169
+ else {
170
+ resolvedRunner = online[0]; // auto-pick the first online runner
171
+ }
172
+ }
173
+ const opts = {
174
+ mode,
175
+ ...(environment !== undefined ? { environment } : {}),
176
+ ...(browser !== undefined ? { browser } : {}),
177
+ ...(headless !== undefined ? { headless } : {}),
178
+ ...(resolvedRunner ? { cloudRunnerId: resolvedRunner.id } : {}),
179
+ ...(strategy !== undefined ? { strategy } : {}),
180
+ ...(maxConcurrent !== undefined ? { maxConcurrent } : {}),
181
+ ...(terminateOnFailure !== undefined ? { terminateOnFailure } : {}),
182
+ };
183
+ try {
184
+ const run = await adapter.startRun({ suiteId }, opts);
185
+ const where = mode === 'cloud'
186
+ ? `on cloud runner ${resolvedRunner ? `**${resolvedRunner.name}**` : ''}`
187
+ : 'locally on the connected agent';
188
+ return ok(`Started suite ${suiteId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
189
+ }
190
+ catch (e) {
191
+ return err(describeError(e));
192
+ }
193
+ });
194
+ server.registerTool('run_status', {
195
+ title: 'Get a run status',
196
+ description: 'Read the status of a run started by tc_run — state (queued/running/passed/failed/stopped/error), ' +
197
+ 'step progress, and a link. Requires a live connection.',
198
+ inputSchema: { runId: z.string().describe('The run id returned by tc_run.') },
199
+ annotations: { readOnlyHint: true },
200
+ }, async ({ runId }) => {
201
+ const adapter = deps.getAdapter();
202
+ if (!adapter)
203
+ return err(NOT_CONNECTED);
204
+ try {
205
+ const status = await adapter.getRun(runId);
206
+ return ok(statusText(status), { run: status });
207
+ }
208
+ catch (e) {
209
+ return err(describeError(e));
210
+ }
211
+ });
212
+ server.registerTool('run_stop', {
213
+ title: 'Stop a run',
214
+ description: 'Signal a running execution to stop. Requires a live connection.',
215
+ inputSchema: { runId: z.string().describe('The run id to stop.') },
216
+ annotations: { readOnlyHint: false },
217
+ }, async ({ runId }) => {
218
+ const adapter = deps.getAdapter();
219
+ if (!adapter)
220
+ return err(NOT_CONNECTED);
221
+ try {
222
+ await adapter.stopRun(runId);
223
+ return ok(`Requested stop for run ${runId}.`);
224
+ }
225
+ catch (e) {
226
+ return err(describeError(e));
227
+ }
228
+ });
229
+ }
230
+ //# sourceMappingURL=run.tools.js.map