common-qa-mcp 0.2.0 → 0.4.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.
- package/README.md +6 -0
- package/dist/adapters/factory.d.ts +17 -7
- package/dist/adapters/factory.d.ts.map +1 -1
- package/dist/adapters/factory.js +44 -7
- package/dist/adapters/factory.js.map +1 -1
- package/dist/adapters/http.d.ts +50 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +108 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/remoteMcp/adapter.d.ts +16 -2
- package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -1
- package/dist/adapters/remoteMcp/adapter.js +34 -5
- package/dist/adapters/remoteMcp/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/adapter.d.ts +437 -0
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/adapter.js +3097 -0
- package/dist/adapters/simplifyqa/adapter.js.map +1 -0
- package/dist/adapters/simplifyqa/mapper.d.ts +95 -0
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/mapper.js +858 -0
- package/dist/adapters/simplifyqa/mapper.js.map +1 -0
- package/dist/adapters/types.d.ts +168 -5
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/walnut/adapter.d.ts +235 -0
- package/dist/adapters/walnut/adapter.d.ts.map +1 -0
- package/dist/adapters/walnut/adapter.js +1114 -0
- package/dist/adapters/walnut/adapter.js.map +1 -0
- package/dist/adapters/walnut/mapper.d.ts +30 -9
- package/dist/adapters/walnut/mapper.d.ts.map +1 -1
- package/dist/adapters/walnut/mapper.js +149 -24
- package/dist/adapters/walnut/mapper.js.map +1 -1
- package/dist/config.d.ts +38 -14
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +73 -19
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +3 -2
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +44 -15
- package/dist/connection.js.map +1 -1
- package/dist/domain/types.d.ts +680 -5
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/drafts/store.d.ts +4 -1
- package/dist/drafts/store.d.ts.map +1 -1
- package/dist/drafts/store.js +7 -2
- package/dist/drafts/store.js.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +48 -18
- package/dist/initAgents.js.map +1 -1
- package/dist/requestConnection.d.ts +5 -3
- package/dist/requestConnection.d.ts.map +1 -1
- package/dist/requestConnection.js +11 -5
- package/dist/requestConnection.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +47 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/backlog.tools.d.ts +13 -0
- package/dist/tools/backlog.tools.d.ts.map +1 -0
- package/dist/tools/backlog.tools.js +399 -0
- package/dist/tools/backlog.tools.js.map +1 -0
- package/dist/tools/connection.tools.js +5 -5
- package/dist/tools/connection.tools.js.map +1 -1
- package/dist/tools/defects.tools.d.ts +7 -0
- package/dist/tools/defects.tools.d.ts.map +1 -0
- package/dist/tools/defects.tools.js +82 -0
- package/dist/tools/defects.tools.js.map +1 -0
- package/dist/tools/discovery.tools.d.ts.map +1 -1
- package/dist/tools/discovery.tools.js +951 -43
- package/dist/tools/discovery.tools.js.map +1 -1
- package/dist/tools/formulas.tools.d.ts +7 -0
- package/dist/tools/formulas.tools.d.ts.map +1 -0
- package/dist/tools/formulas.tools.js +56 -0
- package/dist/tools/formulas.tools.js.map +1 -0
- package/dist/tools/heal.tools.d.ts +17 -0
- package/dist/tools/heal.tools.d.ts.map +1 -0
- package/dist/tools/heal.tools.js +76 -0
- package/dist/tools/heal.tools.js.map +1 -0
- package/dist/tools/markdown.d.ts +3 -3
- package/dist/tools/markdown.d.ts.map +1 -1
- package/dist/tools/markdown.js +89 -18
- package/dist/tools/markdown.js.map +1 -1
- package/dist/tools/objects.tools.d.ts +7 -0
- package/dist/tools/objects.tools.d.ts.map +1 -0
- package/dist/tools/objects.tools.js +201 -0
- package/dist/tools/objects.tools.js.map +1 -0
- package/dist/tools/recording.tools.d.ts.map +1 -1
- package/dist/tools/recording.tools.js +92 -12
- package/dist/tools/recording.tools.js.map +1 -1
- package/dist/tools/run.tools.d.ts.map +1 -1
- package/dist/tools/run.tools.js +464 -39
- package/dist/tools/run.tools.js.map +1 -1
- package/dist/tools/schemas.d.ts +21 -4
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +14 -1
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/storySync.tools.d.ts +66 -0
- package/dist/tools/storySync.tools.d.ts.map +1 -0
- package/dist/tools/storySync.tools.js +213 -0
- package/dist/tools/storySync.tools.js.map +1 -0
- package/dist/tools/tasks.tools.d.ts +7 -0
- package/dist/tools/tasks.tools.d.ts.map +1 -0
- package/dist/tools/tasks.tools.js +75 -0
- package/dist/tools/tasks.tools.js.map +1 -0
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +162 -101
- package/dist/tools/testcase.tools.js.map +1 -1
- package/dist/tools/testdata.tools.d.ts +7 -0
- package/dist/tools/testdata.tools.d.ts.map +1 -0
- package/dist/tools/testdata.tools.js +249 -0
- package/dist/tools/testdata.tools.js.map +1 -0
- package/package.json +3 -3
package/dist/tools/run.tools.js
CHANGED
|
@@ -5,7 +5,63 @@ function describeError(e) {
|
|
|
5
5
|
return e instanceof Error ? e.message : String(e);
|
|
6
6
|
}
|
|
7
7
|
function runnerLine(r) {
|
|
8
|
-
|
|
8
|
+
const host = r.os ? ` · ${r.os}` : '';
|
|
9
|
+
return `- ${r.online ? '🟢' : '⚪'} ${r.name}${host} \`${r.id}\`${r.status ? ` (${r.status})` : ''}`;
|
|
10
|
+
}
|
|
11
|
+
function localAgentLine(a) {
|
|
12
|
+
// Show the machine hostname next to the owner (like the product UI) so the user can tell apart
|
|
13
|
+
// several agents owned by the same person; the credential_id in backticks is for the tool call.
|
|
14
|
+
const host = a.host && a.host !== a.name ? ` · ${a.host}` : '';
|
|
15
|
+
return `- ${a.online ? '🟢' : '⚪'} ${a.name ?? a.id}${host} \`${a.id}\`${a.status ? ` (${a.status})` : ''}`;
|
|
16
|
+
}
|
|
17
|
+
/** One line per schedule for management listings — what it runs, cadence, where + pinned agent,
|
|
18
|
+
* browser, next run, status; the credential-free id in backticks is what the management tools take. */
|
|
19
|
+
function scheduleLine(s) {
|
|
20
|
+
const where = s.executionTarget === 'cloud_runner' ? 'cloud' : 'local';
|
|
21
|
+
const agent = s.agent ? ` → ${s.agent}` : '';
|
|
22
|
+
const br = s.browser ? ` · ${s.browser}` : '';
|
|
23
|
+
const next = s.nextRunAt ? ` · next ${s.nextRunAt}` : s.scheduledFor ? ` · at ${s.scheduledFor}` : '';
|
|
24
|
+
const paused = s.active === false ? ' [paused]' : '';
|
|
25
|
+
return `- ${s.target} — ${s.scheduleType}, ${where}${agent}${br}${next} · **${s.status}**${paused} \`${s.id}\``;
|
|
26
|
+
}
|
|
27
|
+
/** A schedule fires detached from any live session, so a LOCAL schedule must target a specific
|
|
28
|
+
* agent — there is no "your online agent" at fire time. Build the "pick an agent" message, offering
|
|
29
|
+
* ALL agents (online AND offline): a scheduled run can target an agent that is offline right now,
|
|
30
|
+
* because it runs when it fires — this mirrors the product's scheduler picker. */
|
|
31
|
+
async function localScheduleNeedsAgent(adapter) {
|
|
32
|
+
let list = '';
|
|
33
|
+
if (adapter.listLocalAgents) {
|
|
34
|
+
try {
|
|
35
|
+
const agents = await adapter.listLocalAgents();
|
|
36
|
+
list = agents.length
|
|
37
|
+
? '\n\nAgents (a scheduled run can target an offline one — it runs when it fires):\n' +
|
|
38
|
+
agents.map(localAgentLine).join('\n')
|
|
39
|
+
: '\n\nNo local agents are registered to this project.';
|
|
40
|
+
}
|
|
41
|
+
catch { /* best-effort listing — the core message still stands */ }
|
|
42
|
+
}
|
|
43
|
+
return ('A LOCAL schedule runs detached from any live session, so it must target a specific agent. ' +
|
|
44
|
+
'Call again with `agentId` set to a credential_id from local_agents.' + list);
|
|
45
|
+
}
|
|
46
|
+
/** A run-NOW to a specific LOCAL agent cannot target an offline one (mirrors the product UI, which
|
|
47
|
+
* offers only ONLINE agents for an immediate run). Returns an error message when the picked agent is
|
|
48
|
+
* known-offline, else null. Roster errors are swallowed — the backend still validates at reserve. */
|
|
49
|
+
async function localAgentOfflineForNow(adapter, agentId) {
|
|
50
|
+
if (!adapter.listLocalAgents)
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
const agents = await adapter.listLocalAgents();
|
|
54
|
+
const picked = agents.find((a) => a.id === agentId);
|
|
55
|
+
if (picked && !picked.online) {
|
|
56
|
+
const online = agents.filter((a) => a.online);
|
|
57
|
+
return (`Agent \`${agentId}\`${picked.name ? ` (${picked.name})` : ''} is offline — a run-now needs an ONLINE agent. ` +
|
|
58
|
+
(online.length
|
|
59
|
+
? 'Online now:\n' + online.map(localAgentLine).join('\n')
|
|
60
|
+
: 'No local agents are online right now — schedule it instead, or bring an agent online.'));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { /* roster unavailable — let the backend validate at reserve */ }
|
|
64
|
+
return null;
|
|
9
65
|
}
|
|
10
66
|
/** Human summary of a run's status. For a SUITE run `children` carries the per-test-case rollup and
|
|
11
67
|
* `progress` counts TEST CASES; a single test-case run has neither (progress counts steps). */
|
|
@@ -14,10 +70,22 @@ function statusText(s) {
|
|
|
14
70
|
const prog = s.progress ? ` · ${s.progress.done}/${s.progress.total} ${unit}` : '';
|
|
15
71
|
const link = s.reportUrl ? `\n${s.reportUrl}` : '';
|
|
16
72
|
const icon = (state) => (state === 'passed' ? '✅' : state === 'failed' ? '❌' : state === 'running' ? '⏳' : '•');
|
|
73
|
+
// Per-state tally across children (suite runs only). Kept concise — only non-zero buckets, in a
|
|
74
|
+
// stable order so the header reads the same every poll.
|
|
75
|
+
const tally = s.children?.length
|
|
76
|
+
? (() => {
|
|
77
|
+
const order = ['passed', 'failed', 'running', 'queued', 'stopped', 'error'];
|
|
78
|
+
const counts = new Map();
|
|
79
|
+
for (const c of s.children)
|
|
80
|
+
counts.set(c.state, (counts.get(c.state) ?? 0) + 1);
|
|
81
|
+
const parts = order.filter((k) => counts.get(k)).map((k) => `${counts.get(k)} ${k}`);
|
|
82
|
+
return parts.length ? ` · ${parts.join(', ')}` : '';
|
|
83
|
+
})()
|
|
84
|
+
: '';
|
|
17
85
|
const kids = s.children?.length
|
|
18
86
|
? '\n\n' + s.children.map((c) => `- ${icon(c.state)} ${c.id}${c.title ? ` — ${c.title}` : ''} (${c.state})`).join('\n')
|
|
19
87
|
: '';
|
|
20
|
-
return `Run ${s.runId} · **${s.state}**${prog}${link}${kids}`;
|
|
88
|
+
return `Run ${s.runId} · **${s.state}**${prog}${tally}${link}${kids}`;
|
|
21
89
|
}
|
|
22
90
|
export function registerRunTools(server, deps) {
|
|
23
91
|
server.registerTool('cloud_runners', {
|
|
@@ -45,32 +113,118 @@ export function registerRunTools(server, deps) {
|
|
|
45
113
|
return err(describeError(e));
|
|
46
114
|
}
|
|
47
115
|
});
|
|
116
|
+
server.registerTool('local_agents', {
|
|
117
|
+
title: 'List local agents (and which are online)',
|
|
118
|
+
description: 'List the LOCAL agents registered to the connected customer and which are online. Use this ' +
|
|
119
|
+
'to show the user their local run options, or to pick a specific agent id (its credential_id) to ' +
|
|
120
|
+
'pass as `agentId` to a LOCAL tc_run / suite_run. Requires a live connection. ' +
|
|
121
|
+
'NOTE on online status: agents whose `machineName` matches this MCP host are reported online ' +
|
|
122
|
+
'(mirrors sqa-ui\'s own-machine override — the DB `active` flag is only refreshed at first ' +
|
|
123
|
+
'registration + a stale cron, so it lags behind). Other agents\' status still comes from the DB.',
|
|
124
|
+
inputSchema: {},
|
|
125
|
+
annotations: { readOnlyHint: true },
|
|
126
|
+
}, async () => {
|
|
127
|
+
const adapter = deps.getAdapter();
|
|
128
|
+
if (!adapter)
|
|
129
|
+
return err(NOT_CONNECTED);
|
|
130
|
+
if (!adapter.listLocalAgents)
|
|
131
|
+
return err('This product has no local agents to list.');
|
|
132
|
+
try {
|
|
133
|
+
const agents = await adapter.listLocalAgents();
|
|
134
|
+
const online = agents.filter((a) => a.online);
|
|
135
|
+
if (!agents.length)
|
|
136
|
+
return ok('No local agents are registered to this project.', { agents: [] });
|
|
137
|
+
const text = `${agents.length} local agent${agents.length === 1 ? '' : 's'} · ${online.length} online:\n\n` +
|
|
138
|
+
agents.map(localAgentLine).join('\n');
|
|
139
|
+
return ok(text, { agents, online: online.length });
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return err(describeError(e));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
48
145
|
server.registerTool('tc_run', {
|
|
49
|
-
title: 'Run a test case (local or cloud)',
|
|
50
|
-
description:
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
'
|
|
55
|
-
'
|
|
146
|
+
title: 'Run a test case (local or cloud; now or scheduled)',
|
|
147
|
+
description: "Execute ONE existing test case in the connected product. ASK the user for EVERY field the " +
|
|
148
|
+
"Execute dialog asks — never assume — and in ORDER: (1) `releaseId` (from list_releases; " +
|
|
149
|
+
"pass '' if the user skips), (2) `sprintId` (from list_sprints; pass '' if the user skips), " +
|
|
150
|
+
"(3) `versionId` (from tc_get's `versions[]`; pass '' to accept the TC's current version), " +
|
|
151
|
+
'(4) `environment` (from tc_list_environments — required), (5) `browser` (required), ' +
|
|
152
|
+
'(6) `mode` local/cloud, (7) `when` immediate/schedule, (8) for a schedule the start time + ' +
|
|
153
|
+
'recurrence + timezone, (9) THEN offer the eligible agent/runner and pass the pick. The tool ' +
|
|
154
|
+
'REFUSES when any of (1)-(7) is omitted — that is deliberate; it forces the ask instead of ' +
|
|
155
|
+
'silently defaulting. `mode` decides WHERE it runs:\n' +
|
|
156
|
+
" • 'local' — a local agent's own browser. Pass `agentId` from local_agents. For when:'immediate' only " +
|
|
157
|
+
"ONLINE agents are eligible (a run-now can't target an offline agent; omit to auto-pick your own online " +
|
|
158
|
+
"agent). For when:'schedule' ANY agent (online OR offline) is eligible and `agentId` is REQUIRED.\n" +
|
|
159
|
+
" • 'cloud' — an online cloud runner VM (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
160
|
+
'from cloud_runners; for a schedule omit it to let the backend auto-pick a live VM at fire time.\n' +
|
|
161
|
+
'IF THE USER HAS NOT SAID local or cloud, ASK — this tool refuses when `mode` is omitted. For cloud, if ' +
|
|
162
|
+
'no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
163
|
+
'`when` decides WHEN it runs (this tool REFUSES when `when` is omitted — ASK the user):\n' +
|
|
164
|
+
" • 'immediate' — dispatch now; returns the run id — poll run_status for the result.\n" +
|
|
165
|
+
" • 'schedule' — create a schedule instead (requires `scheduledFor`); ASK the start time, recurrence, " +
|
|
166
|
+
'end date and timezone BEFORE offering an agent. Returns the schedule id.\n' +
|
|
167
|
+
'NOTE: MANUAL test cases cannot be run or scheduled by an agent — the backend rejects them.',
|
|
56
168
|
inputSchema: {
|
|
57
169
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
170
|
+
mode: z.enum(['local', 'cloud']).optional().describe("Where to run. REQUIRED — ASK the user if unstated (the tool refuses when omitted)."),
|
|
171
|
+
agentId: z.string().optional().describe("For local: a specific agent credential_id (from local_agents). when:'immediate' → must be an ONLINE agent (or omit to auto-pick your own online agent); when:'schedule' → REQUIRED, and may be an offline agent (it runs when it fires)."),
|
|
172
|
+
releaseId: z.string().optional().describe('Tag the run with a RELEASE (from list_releases). Optional in the UI; omit to leave untagged.'),
|
|
173
|
+
sprintId: z.string().optional().describe('Tag the run with a SPRINT (from list_sprints). Optional in the UI; omit to leave untagged.'),
|
|
174
|
+
versionId: z.string().optional().describe("Override the TC's currently-selected VERSION for this run (an id from the TC's `versions[]`). Omit to run the current version — that's what the UI's Version chip defaults to."),
|
|
175
|
+
environment: z.string().optional().describe('Environment key (from tc_list_environments). ASK the user before running — the UI marks this required.'),
|
|
176
|
+
browser: z.string().optional().describe("Browser, default 'chromium' (bundled + isolated). Others: 'chrome'/'msedge' (installed channel), 'firefox', 'webkit'. NOTE: a schedule accepts only chromium/msedge/firefox/webkit."),
|
|
177
|
+
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
178
|
+
cloudRunnerId: z.string().optional().describe("For cloud: a specific ONLINE runner id (from cloud_runners). For when:'schedule' omit to auto-pick a live VM at fire time."),
|
|
179
|
+
deviceId: z.string().optional().describe("REQUIRED for ANDROID/IOS test cases — the target device id. LOCAL: adb serial or emulator name. CLOUD: the marketplace device name. Enumerate with android_list_devices."),
|
|
180
|
+
when: z.enum(['immediate', 'schedule']).optional().describe("When to run — REQUIRED, ASK the user if unstated (the tool refuses when omitted). 'immediate' dispatches now; 'schedule' creates a schedule (needs scheduledFor)."),
|
|
181
|
+
scheduledFor: z.string().optional().describe("REQUIRED for when:'schedule' — ISO datetime of the first/only run (e.g. '2026-08-20T09:00:00Z')."),
|
|
182
|
+
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe("Recurrence for a schedule. Default 'once'. hourly/daily/weekdays require an endDate."),
|
|
183
|
+
endDate: z.string().optional().describe('For a recurring schedule — ISO end date (required for hourly/daily/weekdays).'),
|
|
184
|
+
timezone: z.string().optional().describe("IANA timezone for a schedule (e.g. 'Asia/Kolkata'). Default 'UTC'."),
|
|
63
185
|
},
|
|
64
186
|
annotations: { readOnlyHint: false },
|
|
65
|
-
}, async ({ tcId, mode, environment, browser, headless, cloudRunnerId }) => {
|
|
187
|
+
}, async ({ tcId, mode, agentId, environment, browser, headless, cloudRunnerId, deviceId, when, scheduledFor, scheduleType, endDate, timezone, releaseId, sprintId, versionId }) => {
|
|
66
188
|
const adapter = deps.getAdapter();
|
|
67
189
|
if (!adapter)
|
|
68
190
|
return err(NOT_CONNECTED);
|
|
191
|
+
// Mirror the Execute-dialog required-field enforcement — ASK the user for every field the UI
|
|
192
|
+
// asks. Release/sprint/version are optional in the UI; pass '' after asking to accept the
|
|
193
|
+
// default (untagged / TC's current version). Environment and browser have red asterisks.
|
|
194
|
+
if (releaseId === undefined) {
|
|
195
|
+
return err("ASK the user which RELEASE this run belongs to (from list_releases), then call tc_run " +
|
|
196
|
+
"again with releaseId. If the user wants no release, pass releaseId: '' (empty string).");
|
|
197
|
+
}
|
|
198
|
+
if (sprintId === undefined) {
|
|
199
|
+
return err("ASK the user which SPRINT this run belongs to (from list_sprints), then call tc_run " +
|
|
200
|
+
"again with sprintId. If the user wants no sprint, pass sprintId: '' (empty string).");
|
|
201
|
+
}
|
|
202
|
+
if (versionId === undefined) {
|
|
203
|
+
return err("ASK the user which VERSION of the test case to run (from tc_get's `versions[]`), then " +
|
|
204
|
+
"call tc_run again with versionId. To accept the TC's currently-selected version, " +
|
|
205
|
+
"pass versionId: '' (empty string).");
|
|
206
|
+
}
|
|
207
|
+
if (!environment) {
|
|
208
|
+
return err('ASK the user which ENVIRONMENT to run against (from tc_list_environments), then call ' +
|
|
209
|
+
'tc_run again with environment set to the chosen env key. Required — the UI marks this ' +
|
|
210
|
+
'field with a red asterisk.');
|
|
211
|
+
}
|
|
212
|
+
if (!browser) {
|
|
213
|
+
return err("ASK the user which BROWSER to run in (chromium / chrome / msedge / firefox / webkit), " +
|
|
214
|
+
'then call tc_run again with browser set. Required — the UI marks this field with a ' +
|
|
215
|
+
'red asterisk.');
|
|
216
|
+
}
|
|
69
217
|
if (!mode) {
|
|
70
218
|
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
|
|
219
|
+
'If the user has not said which, ask them first — local runs on a local agent, ' +
|
|
72
220
|
'cloud runs on an online cloud runner VM.');
|
|
73
221
|
}
|
|
222
|
+
// Never assume now-vs-scheduled — it decides which agents are eligible (online-only for now,
|
|
223
|
+
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
224
|
+
if (!when) {
|
|
225
|
+
return err("Specify WHEN to run: call tc_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
226
|
+
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
227
|
+
}
|
|
74
228
|
let resolvedRunner;
|
|
75
229
|
if (mode === 'cloud') {
|
|
76
230
|
if (!adapter.listCloudRunners)
|
|
@@ -82,6 +236,7 @@ export function registerRunTools(server, deps) {
|
|
|
82
236
|
catch (e) {
|
|
83
237
|
return err(describeError(e));
|
|
84
238
|
}
|
|
239
|
+
// Cloud is ONLINE-only whether now or scheduled (a schedule pin to an offline VM is rejected).
|
|
85
240
|
const online = runners.filter((r) => r.online);
|
|
86
241
|
if (!online.length) {
|
|
87
242
|
return err('Currently there are no active cloud runners in your project.');
|
|
@@ -92,22 +247,74 @@ export function registerRunTools(server, deps) {
|
|
|
92
247
|
return err(`Cloud runner "${cloudRunnerId}" is not online. Online now:\n` + online.map(runnerLine).join('\n'));
|
|
93
248
|
}
|
|
94
249
|
}
|
|
95
|
-
else {
|
|
96
|
-
resolvedRunner = online[0]; // auto-pick the first online runner
|
|
250
|
+
else if (when === 'immediate') {
|
|
251
|
+
resolvedRunner = online[0]; // run-now with no pick → auto-pick the first online runner
|
|
252
|
+
}
|
|
253
|
+
// when:'schedule' with no cloudRunnerId → leave unpinned; the backend auto-picks a live VM at fire time.
|
|
254
|
+
}
|
|
255
|
+
// LOCAL run-NOW to a specific agent must be online (can't dispatch to an offline agent). A local
|
|
256
|
+
// SCHEDULE may target an offline agent — that path is handled in the schedule branch below.
|
|
257
|
+
if (mode === 'local' && when === 'immediate' && agentId) {
|
|
258
|
+
const offlineMsg = await localAgentOfflineForNow(adapter, agentId);
|
|
259
|
+
if (offlineMsg)
|
|
260
|
+
return err(offlineMsg);
|
|
261
|
+
}
|
|
262
|
+
if (when === 'schedule') {
|
|
263
|
+
if (!adapter.createSchedule)
|
|
264
|
+
return err('This product does not support scheduling runs.');
|
|
265
|
+
if (!scheduledFor) {
|
|
266
|
+
return err("Scheduling needs a start time: call tc_run again with when: 'schedule' and scheduledFor as an " +
|
|
267
|
+
"ISO datetime (e.g. '2026-08-20T09:00:00Z'). Ask the user when the run should start (and the " +
|
|
268
|
+
'recurrence, end date and timezone if any).');
|
|
269
|
+
}
|
|
270
|
+
// A schedule fires detached from any live session — a LOCAL schedule must pin a specific agent.
|
|
271
|
+
if (mode === 'local' && !agentId)
|
|
272
|
+
return err(await localScheduleNeedsAgent(adapter));
|
|
273
|
+
// Cloud: pin the runner's CREDENTIAL only when the user explicitly picked one; otherwise leave
|
|
274
|
+
// it unpinned so the backend auto-picks a live VM at fire time (don't freeze today's online[0]).
|
|
275
|
+
const pinnedRunner = mode === 'cloud' && cloudRunnerId ? resolvedRunner : undefined;
|
|
276
|
+
const schedOpts = {
|
|
277
|
+
mode,
|
|
278
|
+
scheduledFor,
|
|
279
|
+
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
280
|
+
...(pinnedRunner?.credentialId ? { cloudRunnerCredentialId: pinnedRunner.credentialId } : {}),
|
|
281
|
+
...(browser !== undefined ? { browser } : {}),
|
|
282
|
+
...(environment !== undefined ? { environmentId: environment } : {}),
|
|
283
|
+
...(releaseId ? { releaseId } : {}),
|
|
284
|
+
...(sprintId ? { sprintId } : {}),
|
|
285
|
+
...(scheduleType !== undefined ? { scheduleType } : {}),
|
|
286
|
+
...(endDate !== undefined ? { endDate } : {}),
|
|
287
|
+
...(timezone !== undefined ? { timezone } : {}),
|
|
288
|
+
};
|
|
289
|
+
try {
|
|
290
|
+
const sched = await adapter.createSchedule({ tcId }, schedOpts);
|
|
291
|
+
const next = sched.nextRunAt ? ` · first run ${sched.nextRunAt}` : '';
|
|
292
|
+
const where = mode === 'cloud'
|
|
293
|
+
? (pinnedRunner ? `cloud runner **${pinnedRunner.name}**` : 'cloud (auto-pick a live VM at run time)')
|
|
294
|
+
: `agent \`${agentId}\``;
|
|
295
|
+
return ok(`Scheduled ${tcId} (${scheduleType ?? 'once'}, ${mode}) for ${scheduledFor}${next} → ${where}. Schedule id \`${sched.id}\`.`, { schedule: sched, mode, when: 'schedule', ...(pinnedRunner ? { runner: pinnedRunner } : {}) });
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
return err(describeError(e));
|
|
97
299
|
}
|
|
98
300
|
}
|
|
99
301
|
const opts = {
|
|
100
302
|
mode,
|
|
303
|
+
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
101
304
|
...(environment !== undefined ? { environment } : {}),
|
|
102
305
|
...(browser !== undefined ? { browser } : {}),
|
|
103
306
|
...(headless !== undefined ? { headless } : {}),
|
|
104
307
|
...(resolvedRunner ? { cloudRunnerId: resolvedRunner.id } : {}),
|
|
308
|
+
...(deviceId !== undefined ? { deviceId } : {}),
|
|
309
|
+
...(releaseId ? { releaseId } : {}),
|
|
310
|
+
...(sprintId ? { sprintId } : {}),
|
|
311
|
+
...(versionId ? { versionId } : {}),
|
|
105
312
|
};
|
|
106
313
|
try {
|
|
107
314
|
const run = await adapter.startRun({ tcId }, opts);
|
|
108
315
|
const where = mode === 'cloud'
|
|
109
316
|
? `on cloud runner ${resolvedRunner ? `**${resolvedRunner.name}**` : ''}`
|
|
110
|
-
:
|
|
317
|
+
: `locally${agentId ? ` on agent \`${agentId}\`` : ' on your online agent'}`;
|
|
111
318
|
return ok(`Started ${tcId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
112
319
|
}
|
|
113
320
|
catch (e) {
|
|
@@ -115,36 +322,83 @@ export function registerRunTools(server, deps) {
|
|
|
115
322
|
}
|
|
116
323
|
});
|
|
117
324
|
server.registerTool('suite_run', {
|
|
118
|
-
title: 'Run a test suite (local or cloud)',
|
|
325
|
+
title: 'Run a test suite (local or cloud; now or scheduled)',
|
|
119
326
|
description: 'Execute an existing test SUITE — all its member test cases, in order — in the connected product. ' +
|
|
327
|
+
'ASK the user for EVERY field the UI\'s Execute-suite dialog asks — never assume — and in ORDER ' +
|
|
328
|
+
'(mirrors the product UI, `_parentComponent==="SU"`): (1) `releaseId` (from list_releases — the UI ' +
|
|
329
|
+
'marks it required with `*` for a suite), (2) `sprintId` (from list_sprints — also `*` for a suite), ' +
|
|
330
|
+
'(3) `environment` (from tc_list_environments — `*`), (4) `mode` local/cloud, (5) `when` immediate/schedule, ' +
|
|
331
|
+
'(6) for a schedule the start time + recurrence + timezone, (7) THEN offer the eligible agent/runner. ' +
|
|
332
|
+
'The tool REFUSES when any of (1)–(5) is omitted — that is deliberate; it forces the ask instead of ' +
|
|
333
|
+
'silently defaulting. UNLIKE `tc_run`, a suite does NOT ask for `versionId` or `browser` — each TC ' +
|
|
334
|
+
"in the suite carries its own pinned version + execution platform (that's what the suite editor picks per row). " +
|
|
120
335
|
'`mode` decides WHERE it runs:\n' +
|
|
121
|
-
" • 'local' —
|
|
122
|
-
"
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
336
|
+
" • 'local' — a local agent's own browser, always serial. Pass `agentId` from local_agents. For " +
|
|
337
|
+
"when:'immediate' only ONLINE agents are eligible (omit to auto-pick your own online agent); for " +
|
|
338
|
+
"when:'schedule' ANY agent (online OR offline) is eligible and `agentId` is REQUIRED.\n" +
|
|
339
|
+
" • 'cloud' — online cloud runner VM(s) (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
340
|
+
'from cloud_runners; for a schedule omit it to auto-pick a live VM at fire time.\n' +
|
|
341
|
+
'For cloud, if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
342
|
+
'`when` decides WHEN it runs:\n' +
|
|
343
|
+
" • 'immediate' — dispatch now; returns the suite-run id — poll run_status for progress " +
|
|
344
|
+
'(per-test-case results under `children`) and run_stop to cancel.\n' +
|
|
345
|
+
" • 'schedule' — create a schedule instead (requires `scheduledFor`); ASK the start time, recurrence, " +
|
|
346
|
+
'end date and timezone BEFORE offering an agent. Returns the schedule id.\n' +
|
|
347
|
+
'NOTE: a suite of only MANUAL test cases cannot be run or scheduled by an agent — the backend rejects it.',
|
|
127
348
|
inputSchema: {
|
|
128
349
|
suiteId: z.string().describe('The suite id to run (from suite_list / suite_get).'),
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
350
|
+
releaseId: z.string().optional().describe('REQUIRED for a suite (UI marks it `*` for _parentComponent==="SU"). A release id from list_releases. Ask the user.'),
|
|
351
|
+
sprintId: z.string().optional().describe('REQUIRED for a suite (UI marks it `*` for _parentComponent==="SU"). A sprint id from list_sprints (scoped to the picked release). Ask the user.'),
|
|
352
|
+
mode: z.enum(['local', 'cloud']).optional().describe('Where to run. REQUIRED — ASK the user if unstated (the tool refuses when omitted).'),
|
|
353
|
+
agentId: z.string().optional().describe("For local: a specific agent credential_id (from local_agents). when:'immediate' → must be ONLINE (or omit to auto-pick your own online agent); when:'schedule' → REQUIRED, may be offline."),
|
|
354
|
+
environment: z.string().optional().describe('Environment key (from tc_list_environments). REQUIRED — the UI marks this with a red asterisk for a suite.'),
|
|
355
|
+
browser: z.string().optional().describe("Optional at the SUITE level — each TC in the suite has its own execution platform (that's the suite-editor pick per row). Provide only to OVERRIDE for every WEB TC. Values: 'chromium' (default), 'chrome'/'msedge', 'firefox', 'webkit'. NOTE: a schedule accepts only chromium/msedge/firefox/webkit."),
|
|
356
|
+
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
357
|
+
cloudRunnerId: z.string().optional().describe("For cloud: a specific ONLINE runner id (from cloud_runners). For when:'schedule' omit to auto-pick a live VM at fire time."),
|
|
358
|
+
strategy: z.enum(['serial', 'batch', 'dynamic']).optional().describe('Cloud dispatch order across the suite (immediate runs). Default serial; local is always serial.'),
|
|
359
|
+
maxConcurrent: z.number().int().positive().optional().describe('Cloud batch/dynamic only — max test cases running at once (immediate runs).'),
|
|
360
|
+
terminateOnFailure: z.boolean().optional().describe('Stop dispatching remaining test cases after the first failure (immediate runs).'),
|
|
361
|
+
when: z.enum(['immediate', 'schedule']).optional().describe("When to run — REQUIRED, ASK the user if unstated (the tool refuses when omitted). 'immediate' dispatches now; 'schedule' creates a schedule (needs scheduledFor)."),
|
|
362
|
+
scheduledFor: z.string().optional().describe("REQUIRED for when:'schedule' — ISO datetime of the first/only run (e.g. '2026-08-20T09:00:00Z')."),
|
|
363
|
+
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe("Recurrence for a schedule. Default 'once'. hourly/daily/weekdays require an endDate."),
|
|
364
|
+
endDate: z.string().optional().describe('For a recurring schedule — ISO end date (required for hourly/daily/weekdays).'),
|
|
365
|
+
timezone: z.string().optional().describe("IANA timezone for a schedule (e.g. 'Asia/Kolkata'). Default 'UTC'."),
|
|
137
366
|
},
|
|
138
367
|
annotations: { readOnlyHint: false },
|
|
139
|
-
}, async ({ suiteId, mode, environment, browser, headless, cloudRunnerId, strategy, maxConcurrent, terminateOnFailure }) => {
|
|
368
|
+
}, async ({ suiteId, releaseId, sprintId, mode, agentId, environment, browser, headless, cloudRunnerId, strategy, maxConcurrent, terminateOnFailure, when, scheduledFor, scheduleType, endDate, timezone }) => {
|
|
140
369
|
const adapter = deps.getAdapter();
|
|
141
370
|
if (!adapter)
|
|
142
371
|
return err(NOT_CONNECTED);
|
|
372
|
+
// Mirror the Execute-suite dialog required-field enforcement (_parentComponent==='SU' branch of
|
|
373
|
+
// execution-popup-modal.component.html) — release, sprint, environment all carry a red `*` for
|
|
374
|
+
// a suite. UNLIKE tc_run, empty-string is NOT accepted: the UI blocks form submit until each is
|
|
375
|
+
// picked. `versionId` and `browser` are TC-level in a suite (each row has its own) so we
|
|
376
|
+
// don't ask for them.
|
|
377
|
+
if (!releaseId) {
|
|
378
|
+
return err("ASK the user which RELEASE this suite run belongs to (from list_releases), then call " +
|
|
379
|
+
'suite_run again with releaseId. Required for a suite — the UI marks it with a red asterisk.');
|
|
380
|
+
}
|
|
381
|
+
if (!sprintId) {
|
|
382
|
+
return err("ASK the user which SPRINT this suite run belongs to (from list_sprints, filtered by " +
|
|
383
|
+
'releaseId), then call suite_run again with sprintId. Required for a suite — the UI marks it ' +
|
|
384
|
+
'with a red asterisk.');
|
|
385
|
+
}
|
|
386
|
+
if (!environment) {
|
|
387
|
+
return err('ASK the user which ENVIRONMENT to run against (from tc_list_environments), then call ' +
|
|
388
|
+
'suite_run again with environment set to the chosen env key. Required — the UI marks this ' +
|
|
389
|
+
'field with a red asterisk for a suite.');
|
|
390
|
+
}
|
|
143
391
|
if (!mode) {
|
|
144
392
|
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
|
|
393
|
+
'If the user has not said which, ask them first — local runs on a local agent, ' +
|
|
146
394
|
'cloud runs on online cloud runner VM(s).');
|
|
147
395
|
}
|
|
396
|
+
// Never assume now-vs-scheduled — it decides which agents are eligible (online-only for now,
|
|
397
|
+
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
398
|
+
if (!when) {
|
|
399
|
+
return err("Specify WHEN to run: call suite_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
400
|
+
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
401
|
+
}
|
|
148
402
|
let resolvedRunner;
|
|
149
403
|
if (mode === 'cloud') {
|
|
150
404
|
if (!adapter.listCloudRunners)
|
|
@@ -156,6 +410,7 @@ export function registerRunTools(server, deps) {
|
|
|
156
410
|
catch (e) {
|
|
157
411
|
return err(describeError(e));
|
|
158
412
|
}
|
|
413
|
+
// Cloud is ONLINE-only whether now or scheduled (a schedule pin to an offline VM is rejected).
|
|
159
414
|
const online = runners.filter((r) => r.online);
|
|
160
415
|
if (!online.length) {
|
|
161
416
|
return err('Currently there are no active cloud runners in your project.');
|
|
@@ -166,16 +421,67 @@ export function registerRunTools(server, deps) {
|
|
|
166
421
|
return err(`Cloud runner "${cloudRunnerId}" is not online. Online now:\n` + online.map(runnerLine).join('\n'));
|
|
167
422
|
}
|
|
168
423
|
}
|
|
169
|
-
else {
|
|
170
|
-
resolvedRunner = online[0]; // auto-pick the first online runner
|
|
424
|
+
else if (when === 'immediate') {
|
|
425
|
+
resolvedRunner = online[0]; // run-now with no pick → auto-pick the first online runner
|
|
426
|
+
}
|
|
427
|
+
// when:'schedule' with no cloudRunnerId → leave unpinned; the backend auto-picks a live VM at fire time.
|
|
428
|
+
}
|
|
429
|
+
// LOCAL run-NOW to a specific agent must be online (can't dispatch to an offline agent). A local
|
|
430
|
+
// SCHEDULE may target an offline agent — that path is handled in the schedule branch below.
|
|
431
|
+
if (mode === 'local' && when === 'immediate' && agentId) {
|
|
432
|
+
const offlineMsg = await localAgentOfflineForNow(adapter, agentId);
|
|
433
|
+
if (offlineMsg)
|
|
434
|
+
return err(offlineMsg);
|
|
435
|
+
}
|
|
436
|
+
if (when === 'schedule') {
|
|
437
|
+
if (!adapter.createSchedule)
|
|
438
|
+
return err('This product does not support scheduling runs.');
|
|
439
|
+
if (!scheduledFor) {
|
|
440
|
+
return err("Scheduling needs a start time: call suite_run again with when: 'schedule' and scheduledFor as an " +
|
|
441
|
+
"ISO datetime (e.g. '2026-08-20T09:00:00Z'). Ask the user when the run should start (and the " +
|
|
442
|
+
'recurrence, end date and timezone if any). NOTE: strategy/maxConcurrent/terminateOnFailure are ' +
|
|
443
|
+
"immediate-run only — a schedule uses the suite's own settings.");
|
|
444
|
+
}
|
|
445
|
+
// A schedule fires detached from any live session — a LOCAL schedule must pin a specific agent.
|
|
446
|
+
if (mode === 'local' && !agentId)
|
|
447
|
+
return err(await localScheduleNeedsAgent(adapter));
|
|
448
|
+
// Cloud: pin the runner's CREDENTIAL only when the user explicitly picked one; otherwise leave
|
|
449
|
+
// it unpinned so the backend auto-picks a live VM at fire time (don't freeze today's online[0]).
|
|
450
|
+
const pinnedRunner = mode === 'cloud' && cloudRunnerId ? resolvedRunner : undefined;
|
|
451
|
+
const schedOpts = {
|
|
452
|
+
mode,
|
|
453
|
+
scheduledFor,
|
|
454
|
+
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
455
|
+
...(pinnedRunner?.credentialId ? { cloudRunnerCredentialId: pinnedRunner.credentialId } : {}),
|
|
456
|
+
...(browser !== undefined ? { browser } : {}),
|
|
457
|
+
...(environment !== undefined ? { environmentId: environment } : {}),
|
|
458
|
+
...(releaseId ? { releaseId } : {}),
|
|
459
|
+
...(sprintId ? { sprintId } : {}),
|
|
460
|
+
...(scheduleType !== undefined ? { scheduleType } : {}),
|
|
461
|
+
...(endDate !== undefined ? { endDate } : {}),
|
|
462
|
+
...(timezone !== undefined ? { timezone } : {}),
|
|
463
|
+
};
|
|
464
|
+
try {
|
|
465
|
+
const sched = await adapter.createSchedule({ suiteId }, schedOpts);
|
|
466
|
+
const next = sched.nextRunAt ? ` · first run ${sched.nextRunAt}` : '';
|
|
467
|
+
const where = mode === 'cloud'
|
|
468
|
+
? (pinnedRunner ? `cloud runner **${pinnedRunner.name}**` : 'cloud (auto-pick a live VM at run time)')
|
|
469
|
+
: `agent \`${agentId}\``;
|
|
470
|
+
return ok(`Scheduled suite ${suiteId} (${scheduleType ?? 'once'}, ${mode}) for ${scheduledFor}${next} → ${where}. Schedule id \`${sched.id}\`.`, { schedule: sched, mode, when: 'schedule', ...(pinnedRunner ? { runner: pinnedRunner } : {}) });
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
return err(describeError(e));
|
|
171
474
|
}
|
|
172
475
|
}
|
|
173
476
|
const opts = {
|
|
174
477
|
mode,
|
|
478
|
+
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
175
479
|
...(environment !== undefined ? { environment } : {}),
|
|
176
480
|
...(browser !== undefined ? { browser } : {}),
|
|
177
481
|
...(headless !== undefined ? { headless } : {}),
|
|
178
482
|
...(resolvedRunner ? { cloudRunnerId: resolvedRunner.id } : {}),
|
|
483
|
+
...(releaseId ? { releaseId } : {}),
|
|
484
|
+
...(sprintId ? { sprintId } : {}),
|
|
179
485
|
...(strategy !== undefined ? { strategy } : {}),
|
|
180
486
|
...(maxConcurrent !== undefined ? { maxConcurrent } : {}),
|
|
181
487
|
...(terminateOnFailure !== undefined ? { terminateOnFailure } : {}),
|
|
@@ -184,7 +490,7 @@ export function registerRunTools(server, deps) {
|
|
|
184
490
|
const run = await adapter.startRun({ suiteId }, opts);
|
|
185
491
|
const where = mode === 'cloud'
|
|
186
492
|
? `on cloud runner ${resolvedRunner ? `**${resolvedRunner.name}**` : ''}`
|
|
187
|
-
:
|
|
493
|
+
: `locally${agentId ? ` on agent \`${agentId}\`` : ' on your online agent'}`;
|
|
188
494
|
return ok(`Started suite ${suiteId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
189
495
|
}
|
|
190
496
|
catch (e) {
|
|
@@ -226,5 +532,124 @@ export function registerRunTools(server, deps) {
|
|
|
226
532
|
return err(describeError(e));
|
|
227
533
|
}
|
|
228
534
|
});
|
|
535
|
+
server.registerTool('schedule_list', {
|
|
536
|
+
title: 'List schedules',
|
|
537
|
+
description: 'List the scheduled runs in the connected project — each with WHAT it runs (test case / suite), ' +
|
|
538
|
+
'its cadence, next run time, status (scheduled / running / paused / cancelled), where it runs + the ' +
|
|
539
|
+
'pinned agent, and its browser. Use this to find a schedule to edit, pause/resume, cancel, or delete. ' +
|
|
540
|
+
'Requires a live connection.',
|
|
541
|
+
inputSchema: {},
|
|
542
|
+
annotations: { readOnlyHint: true },
|
|
543
|
+
}, async () => {
|
|
544
|
+
const adapter = deps.getAdapter();
|
|
545
|
+
if (!adapter)
|
|
546
|
+
return err(NOT_CONNECTED);
|
|
547
|
+
if (!adapter.listSchedules)
|
|
548
|
+
return err('This product does not support listing schedules.');
|
|
549
|
+
try {
|
|
550
|
+
const schedules = await adapter.listSchedules();
|
|
551
|
+
if (!schedules.length)
|
|
552
|
+
return ok('No schedules in this project.', { schedules: [] });
|
|
553
|
+
const text = `${schedules.length} schedule${schedules.length === 1 ? '' : 's'}:\n\n` +
|
|
554
|
+
schedules.map(scheduleLine).join('\n');
|
|
555
|
+
return ok(text, { schedules });
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
return err(describeError(e));
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
server.registerTool('schedule_update', {
|
|
562
|
+
title: 'Edit a schedule',
|
|
563
|
+
description: "Change a schedule's time / recurrence / end date / timezone / browser / environment, or RE-PIN its " +
|
|
564
|
+
'agent. Pass `scheduleId` (from schedule_list) and ONLY the fields to change. To re-pin: ' +
|
|
565
|
+
"mode:'local' + `agentId` (from local_agents), or mode:'cloud' + a runner's `cloudRunnerCredentialId` " +
|
|
566
|
+
'(the `credentialId` from cloud_runners). Requires a live connection.',
|
|
567
|
+
inputSchema: {
|
|
568
|
+
scheduleId: z.string().describe('The schedule id (from schedule_list).'),
|
|
569
|
+
scheduledFor: z.string().optional().describe('New start / next-run ISO datetime.'),
|
|
570
|
+
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe('New recurrence.'),
|
|
571
|
+
endDate: z.string().optional().describe('New ISO end date (for recurring schedules).'),
|
|
572
|
+
timezone: z.string().optional().describe("New IANA timezone (e.g. 'Asia/Kolkata')."),
|
|
573
|
+
browser: z.enum(['chromium', 'msedge', 'firefox', 'webkit']).optional().describe('New browser (schedules accept chromium/msedge/firefox/webkit, not chrome).'),
|
|
574
|
+
environment: z.string().optional().describe('New environment id (Mongo id).'),
|
|
575
|
+
mode: z.enum(['local', 'cloud']).optional().describe('Re-pin target: pair with agentId (local) or cloudRunnerCredentialId (cloud).'),
|
|
576
|
+
agentId: z.string().optional().describe('Local re-pin: an agent credential_id from local_agents.'),
|
|
577
|
+
cloudRunnerCredentialId: z.string().optional().describe("Cloud re-pin: a runner's credentialId from cloud_runners."),
|
|
578
|
+
},
|
|
579
|
+
annotations: { readOnlyHint: false },
|
|
580
|
+
}, async ({ scheduleId, scheduledFor, scheduleType, endDate, timezone, browser, environment, mode, agentId, cloudRunnerCredentialId }) => {
|
|
581
|
+
const adapter = deps.getAdapter();
|
|
582
|
+
if (!adapter)
|
|
583
|
+
return err(NOT_CONNECTED);
|
|
584
|
+
if (!adapter.updateSchedule)
|
|
585
|
+
return err('This product does not support editing schedules.');
|
|
586
|
+
const changes = {
|
|
587
|
+
...(scheduledFor !== undefined ? { scheduledFor } : {}),
|
|
588
|
+
...(scheduleType !== undefined ? { scheduleType } : {}),
|
|
589
|
+
...(endDate !== undefined ? { endDate } : {}),
|
|
590
|
+
...(timezone !== undefined ? { timezone } : {}),
|
|
591
|
+
...(browser !== undefined ? { browser } : {}),
|
|
592
|
+
...(environment !== undefined ? { environmentId: environment } : {}),
|
|
593
|
+
...(mode !== undefined ? { mode } : {}),
|
|
594
|
+
...(agentId !== undefined ? { agentId } : {}),
|
|
595
|
+
...(cloudRunnerCredentialId !== undefined ? { cloudRunnerCredentialId } : {}),
|
|
596
|
+
};
|
|
597
|
+
if (Object.keys(changes).length === 0) {
|
|
598
|
+
return err('Nothing to change — pass at least one field to edit (e.g. scheduledFor, scheduleType, browser, or a re-pin via mode + agentId/cloudRunnerCredentialId).');
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const ref = await adapter.updateSchedule(scheduleId, changes);
|
|
602
|
+
return ok(`Updated schedule \`${ref.id}\`${ref.nextRunAt ? ` · next run ${ref.nextRunAt}` : ''}.`, { schedule: ref });
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
return err(describeError(e));
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
server.registerTool('schedule_control', {
|
|
609
|
+
title: 'Pause / resume / cancel a schedule',
|
|
610
|
+
description: "Change a schedule's state: `pause` (stop it firing but keep it — reversible with `resume`), " +
|
|
611
|
+
'`resume` (re-enable a paused schedule), or `cancel` (stop its pending / live run). To remove a ' +
|
|
612
|
+
'schedule entirely, use schedule_delete instead. Requires a live connection.',
|
|
613
|
+
inputSchema: {
|
|
614
|
+
scheduleId: z.string().describe('The schedule id (from schedule_list).'),
|
|
615
|
+
action: z.enum(['pause', 'resume', 'cancel']).describe('pause / resume / cancel.'),
|
|
616
|
+
},
|
|
617
|
+
annotations: { readOnlyHint: false },
|
|
618
|
+
}, async ({ scheduleId, action }) => {
|
|
619
|
+
const adapter = deps.getAdapter();
|
|
620
|
+
if (!adapter)
|
|
621
|
+
return err(NOT_CONNECTED);
|
|
622
|
+
if (!adapter.controlSchedule)
|
|
623
|
+
return err('This product does not support pausing / resuming / cancelling schedules.');
|
|
624
|
+
try {
|
|
625
|
+
const ref = await adapter.controlSchedule(scheduleId, action);
|
|
626
|
+
const verb = action === 'pause' ? 'Paused' : action === 'resume' ? 'Resumed' : 'Cancelled';
|
|
627
|
+
return ok(`${verb} schedule \`${ref.id}\`${ref.status ? ` · status ${ref.status}` : ''}.`, { schedule: ref });
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
return err(describeError(e));
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
server.registerTool('schedule_delete', {
|
|
634
|
+
title: 'Delete a schedule',
|
|
635
|
+
description: 'Permanently DELETE a schedule (and tear down any run it currently owns). This cannot be undone — ' +
|
|
636
|
+
'to only stop it temporarily use schedule_control (pause / cancel). CONFIRM with the user before ' +
|
|
637
|
+
'deleting. Requires a live connection.',
|
|
638
|
+
inputSchema: { scheduleId: z.string().describe('The schedule id (from schedule_list).') },
|
|
639
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
640
|
+
}, async ({ scheduleId }) => {
|
|
641
|
+
const adapter = deps.getAdapter();
|
|
642
|
+
if (!adapter)
|
|
643
|
+
return err(NOT_CONNECTED);
|
|
644
|
+
if (!adapter.deleteSchedule)
|
|
645
|
+
return err('This product does not support deleting schedules.');
|
|
646
|
+
try {
|
|
647
|
+
await adapter.deleteSchedule(scheduleId);
|
|
648
|
+
return ok(`Deleted schedule \`${scheduleId}\`.`);
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
return err(describeError(e));
|
|
652
|
+
}
|
|
653
|
+
});
|
|
229
654
|
}
|
|
230
655
|
//# sourceMappingURL=run.tools.js.map
|