@browserstack/mcp-server 1.4.0-beta.2 → 1.4.0-beta.3
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/config.d.ts +4 -1
- package/dist/config.js +23 -2
- package/dist/tools/ask-browserstack/central-oauth.d.ts +7 -1
- package/dist/tools/ask-browserstack/central-oauth.js +25 -19
- package/dist/tools/ask-browserstack/config.d.ts +7 -1
- package/dist/tools/ask-browserstack/config.js +11 -5
- package/dist/tools/ask-browserstack/register.js +14 -1
- package/dist/tools/ask-browserstack/stream.js +9 -10
- package/dist/tools/ask-browserstack/types.d.ts +1 -1
- package/dist/tools/ask-browserstack/types.js +5 -1
- package/dist/tools/testmanagement.js +10 -6
- package/dist/tools/tool-handoff.d.ts +25 -0
- package/dist/tools/tool-handoff.js +33 -5
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -11,7 +11,10 @@ export declare class Config {
|
|
|
11
11
|
readonly O11Y_TFA_RCA_BASE_URL: string;
|
|
12
12
|
readonly BROWSERSTACK_AUTOMATION_BASE_URL: string;
|
|
13
13
|
readonly BROWSERSTACK_O11Y_UI_BASE_URL: string;
|
|
14
|
-
|
|
14
|
+
readonly ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY: boolean;
|
|
15
|
+
readonly ASK_BROWSERSTACK_ATLAS_URL: string | undefined;
|
|
16
|
+
readonly ASK_BROWSERSTACK_AUTH_TOKEN_URL: string | undefined;
|
|
17
|
+
constructor(DEV_MODE: boolean, browserstackLocalOptions: Record<string, any>, USE_OWN_LOCAL_BINARY_PROCESS: boolean, REMOTE_MCP: boolean, UPLOAD_BASE_DIR: string | undefined, O11Y_TFA_RCA_BASE_URL: string, BROWSERSTACK_AUTOMATION_BASE_URL: string, BROWSERSTACK_O11Y_UI_BASE_URL: string, ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY: boolean, ASK_BROWSERSTACK_ATLAS_URL: string | undefined, ASK_BROWSERSTACK_AUTH_TOKEN_URL: string | undefined);
|
|
15
18
|
}
|
|
16
19
|
declare const config: Config;
|
|
17
20
|
export default config;
|
package/dist/config.js
CHANGED
|
@@ -47,7 +47,18 @@ export class Config {
|
|
|
47
47
|
O11Y_TFA_RCA_BASE_URL;
|
|
48
48
|
BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
49
49
|
BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
50
|
-
|
|
50
|
+
ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY;
|
|
51
|
+
ASK_BROWSERSTACK_ATLAS_URL;
|
|
52
|
+
ASK_BROWSERSTACK_AUTH_TOKEN_URL;
|
|
53
|
+
constructor(DEV_MODE, browserstackLocalOptions, USE_OWN_LOCAL_BINARY_PROCESS, REMOTE_MCP, UPLOAD_BASE_DIR, O11Y_TFA_RCA_BASE_URL, BROWSERSTACK_AUTOMATION_BASE_URL, BROWSERSTACK_O11Y_UI_BASE_URL,
|
|
54
|
+
// askBrowserStackAI's process-startup settings. Declared here rather than read from
|
|
55
|
+
// process.env inside src/tools/, per rules/tool-design.md — and so the remote wrapper,
|
|
56
|
+
// which only forwards env it knows about, has one place to look.
|
|
57
|
+
//
|
|
58
|
+
// ASK_BROWSERSTACK_DISABLED is deliberately NOT here: it is a kill switch, and reading
|
|
59
|
+
// it per call keeps it effective without a restart. Fixing it at boot would mean a pod
|
|
60
|
+
// roll to disable the tool, which is slowest exactly when you need it fastest.
|
|
61
|
+
ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY, ASK_BROWSERSTACK_ATLAS_URL, ASK_BROWSERSTACK_AUTH_TOKEN_URL) {
|
|
51
62
|
this.DEV_MODE = DEV_MODE;
|
|
52
63
|
this.browserstackLocalOptions = browserstackLocalOptions;
|
|
53
64
|
this.USE_OWN_LOCAL_BINARY_PROCESS = USE_OWN_LOCAL_BINARY_PROCESS;
|
|
@@ -56,6 +67,9 @@ export class Config {
|
|
|
56
67
|
this.O11Y_TFA_RCA_BASE_URL = O11Y_TFA_RCA_BASE_URL;
|
|
57
68
|
this.BROWSERSTACK_AUTOMATION_BASE_URL = BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
58
69
|
this.BROWSERSTACK_O11Y_UI_BASE_URL = BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
70
|
+
this.ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY = ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY;
|
|
71
|
+
this.ASK_BROWSERSTACK_ATLAS_URL = ASK_BROWSERSTACK_ATLAS_URL;
|
|
72
|
+
this.ASK_BROWSERSTACK_AUTH_TOKEN_URL = ASK_BROWSERSTACK_AUTH_TOKEN_URL;
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
75
|
const config = new Config(process.env.DEV_MODE === "true", browserstackLocalOptions, process.env.USE_OWN_LOCAL_BINARY_PROCESS === "true", process.env.REMOTE_MCP === "true", process.env.MCP_UPLOAD_BASE_DIR && process.env.MCP_UPLOAD_BASE_DIR.length > 0
|
|
@@ -69,5 +83,12 @@ const config = new Config(process.env.DEV_MODE === "true", browserstackLocalOpti
|
|
|
69
83
|
: DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL, process.env.BROWSERSTACK_O11Y_UI_BASE_URL &&
|
|
70
84
|
process.env.BROWSERSTACK_O11Y_UI_BASE_URL.length > 0
|
|
71
85
|
? process.env.BROWSERSTACK_O11Y_UI_BASE_URL
|
|
72
|
-
: DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL)
|
|
86
|
+
: DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL, (process.env.ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY || "").toLowerCase() ===
|
|
87
|
+
"true", process.env.ASK_BROWSERSTACK_ATLAS_URL &&
|
|
88
|
+
process.env.ASK_BROWSERSTACK_ATLAS_URL.trim().length > 0
|
|
89
|
+
? process.env.ASK_BROWSERSTACK_ATLAS_URL
|
|
90
|
+
: undefined, process.env.ASK_BROWSERSTACK_AUTH_TOKEN_URL &&
|
|
91
|
+
process.env.ASK_BROWSERSTACK_AUTH_TOKEN_URL.trim().length > 0
|
|
92
|
+
? process.env.ASK_BROWSERSTACK_AUTH_TOKEN_URL
|
|
93
|
+
: undefined);
|
|
73
94
|
export default config;
|
|
@@ -101,7 +101,13 @@ export declare const AUTH_SERVER_ERROR_DETAIL: (status: number) => string;
|
|
|
101
101
|
export declare const AUTH_UNUSABLE_DETAIL: (status: number) => string;
|
|
102
102
|
/** Drop every cached token. For tests, and for a credential rotation. */
|
|
103
103
|
export declare function resetTokenCache(): void;
|
|
104
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* The token endpoint, through `apiClient` per rules/security.md — no bare `fetch`.
|
|
106
|
+
*
|
|
107
|
+
* `raise_error: false` keeps the status-first contract this transport has always had: the
|
|
108
|
+
* caller distinguishes a 400 scope refusal from a 401 rejection from an unreachable host,
|
|
109
|
+
* so a thrown AxiosError on any non-2xx would destroy the only signal it reads.
|
|
110
|
+
*/
|
|
105
111
|
export declare function fetchTokenTransport(timeoutMs?: number): TokenTransport;
|
|
106
112
|
/** The exact form body of the `client_credentials` grant. */
|
|
107
113
|
export declare function mintForm(credentials: Credentials): Record<string, string>;
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* logged, returned, or put in an error message. Only a status code is.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
18
|
+
import appConfig from "../../config.js";
|
|
17
19
|
import logger from "../../logger.js";
|
|
18
20
|
import { AGENT_TIMEOUT_MS, AskError } from "./config.js";
|
|
19
21
|
/**
|
|
@@ -157,40 +159,33 @@ function cacheKey(url, credentials) {
|
|
|
157
159
|
.digest("hex");
|
|
158
160
|
return `${url} ${credentials.username} ${CENTRAL_SCOPE} ${digest}`;
|
|
159
161
|
}
|
|
160
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* The token endpoint, through `apiClient` per rules/security.md — no bare `fetch`.
|
|
164
|
+
*
|
|
165
|
+
* `raise_error: false` keeps the status-first contract this transport has always had: the
|
|
166
|
+
* caller distinguishes a 400 scope refusal from a 401 rejection from an unreachable host,
|
|
167
|
+
* so a thrown AxiosError on any non-2xx would destroy the only signal it reads.
|
|
168
|
+
*/
|
|
161
169
|
export function fetchTokenTransport(timeoutMs = TOKEN_TIMEOUT_MS) {
|
|
162
170
|
return async (url, form) => {
|
|
163
|
-
const controller = new AbortController();
|
|
164
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
165
171
|
try {
|
|
166
|
-
const response = await
|
|
167
|
-
|
|
172
|
+
const response = await apiClient.post({
|
|
173
|
+
url,
|
|
168
174
|
headers: {
|
|
169
175
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
170
176
|
Accept: "application/json",
|
|
171
177
|
},
|
|
172
178
|
body: new URLSearchParams(form).toString(),
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
timeout: timeoutMs,
|
|
180
|
+
raise_error: false,
|
|
175
181
|
});
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
parsed = await response.json();
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
// An HTML error page behind any status. The caller only reads the status.
|
|
182
|
-
parsed = null;
|
|
183
|
-
}
|
|
184
|
-
return { status: response.status, body: parsed };
|
|
182
|
+
return { status: response.status, body: response.data ?? null };
|
|
185
183
|
}
|
|
186
184
|
catch {
|
|
187
185
|
// DNS, TLS, timeout — all of them mean "no token". The reason is deliberately not
|
|
188
186
|
// carried: it can name the URL and, on some stacks, echo the request body.
|
|
189
187
|
return { status: 0, body: null, error: "auth could not be reached" };
|
|
190
188
|
}
|
|
191
|
-
finally {
|
|
192
|
-
clearTimeout(timer);
|
|
193
|
-
}
|
|
194
189
|
};
|
|
195
190
|
}
|
|
196
191
|
/** The exact form body of the `client_credentials` grant. */
|
|
@@ -247,6 +242,17 @@ export async function mintCentralToken(url, credentials, transport, now = Date.n
|
|
|
247
242
|
throw new AskError("BrowserStack AI is not authenticated: BROWSERSTACK_USERNAME and " +
|
|
248
243
|
"BROWSERSTACK_ACCESS_KEY are required to sign in");
|
|
249
244
|
}
|
|
245
|
+
// NOT CACHED IN HOSTED MODE. These tokens are per-user, attested credentials, and the
|
|
246
|
+
// process is shared by every tenant — `rules/multi-tenant-safety.md` forbids holding user
|
|
247
|
+
// data in module-level state there, so remote mode mints per call. Keying on
|
|
248
|
+
// username + sha256(accessKey) already means one user can never be SERVED another's token,
|
|
249
|
+
// but containment is not the contract; not holding it at all is.
|
|
250
|
+
if (appConfig.REMOTE_MCP) {
|
|
251
|
+
return mintOnce(url, credentials, transport).then(({ token }) => {
|
|
252
|
+
logger.info("askBrowserStackAI: signed in as %s", credentials.username);
|
|
253
|
+
return token;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
250
256
|
const key = cacheKey(url, credentials);
|
|
251
257
|
const entry = cache.get(key);
|
|
252
258
|
if (entry && entry.token && now < entry.expiresAt - REFRESH_SKEW_MS) {
|
|
@@ -22,7 +22,13 @@ export declare const ELICITATION_TIMEOUT_MS = 270000;
|
|
|
22
22
|
/** Thrown for anything this tool refuses to attempt. Never carries a credential. */
|
|
23
23
|
export declare class AskError extends Error {
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Off by default is wrong for a shipped feature, but a kill switch is not.
|
|
27
|
+
*
|
|
28
|
+
* The only setting here still read from `process.env` per call, and deliberately: a kill
|
|
29
|
+
* switch that needs a process restart is slowest exactly when it is needed fastest. The
|
|
30
|
+
* other three are on the config singleton (rules/tool-design.md).
|
|
31
|
+
*/
|
|
26
32
|
export declare function isEnabled(): boolean;
|
|
27
33
|
/**
|
|
28
34
|
* May the relay be offered in the hosted (`REMOTE_MCP`) deployment?
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import appConfig from "../../config.js";
|
|
1
2
|
import logger from "../../logger.js";
|
|
2
3
|
/**
|
|
3
4
|
* Where Atlas lives, and the timeout ladder.
|
|
@@ -23,7 +24,13 @@ export const ELICITATION_TIMEOUT_MS = 270_000;
|
|
|
23
24
|
/** Thrown for anything this tool refuses to attempt. Never carries a credential. */
|
|
24
25
|
export class AskError extends Error {
|
|
25
26
|
}
|
|
26
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Off by default is wrong for a shipped feature, but a kill switch is not.
|
|
29
|
+
*
|
|
30
|
+
* The only setting here still read from `process.env` per call, and deliberately: a kill
|
|
31
|
+
* switch that needs a process restart is slowest exactly when it is needed fastest. The
|
|
32
|
+
* other three are on the config singleton (rules/tool-design.md).
|
|
33
|
+
*/
|
|
27
34
|
export function isEnabled() {
|
|
28
35
|
return (process.env.ASK_BROWSERSTACK_DISABLED || "").toLowerCase() !== "true";
|
|
29
36
|
}
|
|
@@ -41,8 +48,7 @@ export function isEnabled() {
|
|
|
41
48
|
* run. This flag only removes the blanket refusal.
|
|
42
49
|
*/
|
|
43
50
|
export function allowRemoteRelay() {
|
|
44
|
-
return
|
|
45
|
-
"true");
|
|
51
|
+
return appConfig.ASK_BROWSERSTACK_ALLOW_REMOTE_RELAY;
|
|
46
52
|
}
|
|
47
53
|
/**
|
|
48
54
|
* ============================================================================
|
|
@@ -111,7 +117,7 @@ function announce(what, url, source) {
|
|
|
111
117
|
* no selector: one default, one override.
|
|
112
118
|
*/
|
|
113
119
|
export function atlasBaseUrl() {
|
|
114
|
-
const explicit =
|
|
120
|
+
const explicit = appConfig.ASK_BROWSERSTACK_ATLAS_URL;
|
|
115
121
|
const url = explicit && explicit.trim() ? trimUrl(explicit) : DEFAULT_ATLAS_URL;
|
|
116
122
|
announce("Atlas", url, explicit && explicit.trim() ? "env" : "default");
|
|
117
123
|
return url;
|
|
@@ -127,7 +133,7 @@ export function agentUrl() {
|
|
|
127
133
|
* the only way in. Same two rungs as the host, and the same staging default.
|
|
128
134
|
*/
|
|
129
135
|
export function authTokenUrl() {
|
|
130
|
-
const explicit =
|
|
136
|
+
const explicit = appConfig.ASK_BROWSERSTACK_AUTH_TOKEN_URL;
|
|
131
137
|
const url = explicit && explicit.trim() ? trimUrl(explicit) : DEFAULT_AUTH_TOKEN_URL;
|
|
132
138
|
announce("auth token endpoint", url, explicit && explicit.trim() ? "env" : "default");
|
|
133
139
|
return url;
|
|
@@ -312,7 +312,7 @@ export function addAskBrowserStackAITool(server, deps, config) {
|
|
|
312
312
|
tools.askBrowserStackAI = server.tool("askBrowserStackAI", DESCRIPTION, {
|
|
313
313
|
product: z
|
|
314
314
|
.enum(PRODUCTS)
|
|
315
|
-
.describe("Which product to work in: tm (Test Management),
|
|
315
|
+
.describe("Which product to work in: tm (Test Management), " +
|
|
316
316
|
"tra (Test Reporting & Analytics)."),
|
|
317
317
|
query: z
|
|
318
318
|
.string()
|
|
@@ -323,6 +323,11 @@ export function addAskBrowserStackAITool(server, deps, config) {
|
|
|
323
323
|
// sets it false: consent is not a licence to delete.
|
|
324
324
|
readOnlyHint: false,
|
|
325
325
|
destructiveHint: false,
|
|
326
|
+
// openWorldHint: the agent fans out to product APIs chosen at runtime, so the set of
|
|
327
|
+
// effects is not knowable from this schema. idempotentHint false because a repeated
|
|
328
|
+
// call can create a second record — the relay asks again, it does not dedupe.
|
|
329
|
+
openWorldHint: true,
|
|
330
|
+
idempotentHint: false,
|
|
326
331
|
title: "Ask BrowserStack AI (Alpha)",
|
|
327
332
|
}, async ({ product, query }, extra) => {
|
|
328
333
|
track("askBrowserStackAI");
|
|
@@ -371,6 +376,14 @@ export function addAskBrowserStackAITool(server, deps, config) {
|
|
|
371
376
|
? error.message
|
|
372
377
|
: String(error);
|
|
373
378
|
logger.error("askBrowserStackAI failed: %s", message);
|
|
379
|
+
// Error telemetry, in the same never-fatal shape as the success-path `track()`:
|
|
380
|
+
// a failing tool call must not be made worse by a failing analytics call.
|
|
381
|
+
try {
|
|
382
|
+
trackMCP("askBrowserStackAI", server.server.getClientVersion(), error, config);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
/* ignore */
|
|
386
|
+
}
|
|
374
387
|
// No `canElicit` argument: the request never left this process, so whether the
|
|
375
388
|
// client could have been prompted is not what the reader needs to know.
|
|
376
389
|
return toResult(errorResult(message, approvals));
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* is a pipe. Keeping the judgement out of the transport is why swapping A2 for A1 does
|
|
23
23
|
* not risk the fail-closed behaviour.
|
|
24
24
|
*/
|
|
25
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
25
26
|
import logger from "../../logger.js";
|
|
26
27
|
import { AskError } from "./config.js";
|
|
27
28
|
/**
|
|
@@ -208,15 +209,16 @@ export function fetchAgentStreamTransport(timeoutMs = WHOLE_RUN_TIMEOUT_MS) {
|
|
|
208
209
|
/** The decision POST. 30s, because it is an ordinary short request. */
|
|
209
210
|
export function fetchDecisionTransport(timeoutMs = 30_000) {
|
|
210
211
|
return async (url, headers, body) => {
|
|
211
|
-
const controller = new AbortController();
|
|
212
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
213
212
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
// Through `apiClient` per rules/security.md. `raise_error: false` because the caller
|
|
214
|
+
// reads the STATUS: a 404 (run gone) and a 409 (already decided) are both answers,
|
|
215
|
+
// and a thrown AxiosError would collapse them into the unreachable case below.
|
|
216
|
+
const response = await apiClient.post({
|
|
217
|
+
url,
|
|
216
218
|
headers,
|
|
217
|
-
body
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
body,
|
|
220
|
+
timeout: timeoutMs,
|
|
221
|
+
raise_error: false,
|
|
220
222
|
});
|
|
221
223
|
return response.status;
|
|
222
224
|
}
|
|
@@ -226,9 +228,6 @@ export function fetchDecisionTransport(timeoutMs = 30_000) {
|
|
|
226
228
|
// the caller can say that rather than implying a human refused.
|
|
227
229
|
return 0;
|
|
228
230
|
}
|
|
229
|
-
finally {
|
|
230
|
-
clearTimeout(timer);
|
|
231
|
-
}
|
|
232
231
|
};
|
|
233
232
|
}
|
|
234
233
|
/** `POST /agent/{run_id}/permission`, built from the base URL the tool already resolved. */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* different repo, at the same time. A field renamed here to read better is a field the
|
|
7
7
|
* other half will never send. Nothing here changes without changing that document first.
|
|
8
8
|
*/
|
|
9
|
-
export declare const PRODUCTS: readonly ["tm", "
|
|
9
|
+
export declare const PRODUCTS: readonly ["tm", "tra"];
|
|
10
10
|
export type Product = (typeof PRODUCTS)[number];
|
|
11
11
|
/** CONTRACT §2 — what Atlas emits on the run's stream when its gate needs a human. */
|
|
12
12
|
export interface PermissionAsk {
|
|
@@ -6,5 +6,9 @@
|
|
|
6
6
|
* different repo, at the same time. A field renamed here to read better is a field the
|
|
7
7
|
* other half will never send. Nothing here changes without changing that document first.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
// a11y is deliberately ABSENT while Ask AI is in limited alpha. Atlas itself serves the
|
|
10
|
+
// product; this tool just does not offer it yet. Re-adding it means this list, the `product`
|
|
11
|
+
// describe text in register.ts, and the two a11y handoffs in tool-handoff.ts — which stop
|
|
12
|
+
// pointing here precisely because the call would now be rejected.
|
|
13
|
+
export const PRODUCTS = ["tm", "tra"];
|
|
10
14
|
export const ASK_STATUSES = ["ok", "blocked", "error", "rate_limited"];
|
|
@@ -19,7 +19,7 @@ import { getTestPlan, GetTestPlanSchema, } from "./testmanagement-utils/get-test
|
|
|
19
19
|
import { listSubTestPlans, ListSubTestPlansSchema, } from "./testmanagement-utils/list-sub-testplans.js";
|
|
20
20
|
import { getSubTestPlan, GetSubTestPlanSchema, } from "./testmanagement-utils/get-sub-testplan.js";
|
|
21
21
|
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
|
|
22
|
-
import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID } from "./tool-handoff.js";
|
|
22
|
+
import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID, PLAN_WRITES_VIA_AGENT, PROJECT_ID_ONLY_FOR_FOLDER, } from "./tool-handoff.js";
|
|
23
23
|
//TODO: Moving the traceMCP and catch block to the parent(server) function
|
|
24
24
|
/**
|
|
25
25
|
* Wrapper to call createProjectOrFolder util.
|
|
@@ -434,7 +434,7 @@ export async function getSubTestPlanTool(args, config, server) {
|
|
|
434
434
|
export default function addTestManagementTools(server, config) {
|
|
435
435
|
const tools = {};
|
|
436
436
|
tools.createProjectOrFolder = server.tool("createProjectOrFolder", "Create a project and/or folder in BrowserStack Test Management." +
|
|
437
|
-
|
|
437
|
+
PROJECT_ID_ONLY_FOR_FOLDER, CreateProjFoldSchema.shape, {
|
|
438
438
|
title: "Create Project or Folder",
|
|
439
439
|
readOnlyHint: false,
|
|
440
440
|
openWorldHint: false,
|
|
@@ -535,7 +535,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
535
535
|
idempotentHint: false,
|
|
536
536
|
}, (args, context) => createLCAStepsTool(args, context, config, server));
|
|
537
537
|
tools.listTestPlans = server.tool("listTestPlans", "List test plans in a BrowserStack Test Management project. Returns each plan's identifier (TP-*), name, status, description, dates, and active/closed test-run counts. Supports pagination." +
|
|
538
|
-
NEEDS_PROJECT_ID
|
|
538
|
+
NEEDS_PROJECT_ID +
|
|
539
|
+
PLAN_WRITES_VIA_AGENT, ListTestPlansSchema.shape, {
|
|
539
540
|
title: "List Test Plans",
|
|
540
541
|
readOnlyHint: true,
|
|
541
542
|
openWorldHint: false,
|
|
@@ -544,7 +545,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
544
545
|
}, (args) => listTestPlansTool(args, config, server));
|
|
545
546
|
tools.getTestPlan = server.tool("getTestPlan", "Fetch a test plan by identifier (TP-*) from BrowserStack Test Management. Returns plan metadata, the full list of linked test runs, total test-case count across runs, and a status summary — suitable for generating test documentation or QA status reports." +
|
|
546
547
|
NEEDS_PROJECT_ID +
|
|
547
|
-
NEEDS_TEST_PLAN_ID
|
|
548
|
+
NEEDS_TEST_PLAN_ID +
|
|
549
|
+
PLAN_WRITES_VIA_AGENT, GetTestPlanSchema.shape, {
|
|
548
550
|
title: "Get Test Plan",
|
|
549
551
|
readOnlyHint: true,
|
|
550
552
|
openWorldHint: false,
|
|
@@ -553,7 +555,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
553
555
|
}, (args) => getTestPlanTool(args, config, server));
|
|
554
556
|
tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination." +
|
|
555
557
|
NEEDS_PROJECT_ID +
|
|
556
|
-
NEEDS_TEST_PLAN_ID
|
|
558
|
+
NEEDS_TEST_PLAN_ID +
|
|
559
|
+
PLAN_WRITES_VIA_AGENT, ListSubTestPlansSchema.shape, {
|
|
557
560
|
title: "List Sub Test Plans",
|
|
558
561
|
readOnlyHint: true,
|
|
559
562
|
openWorldHint: false,
|
|
@@ -562,7 +565,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
562
565
|
}, (args) => listSubTestPlansTool(args, config, server));
|
|
563
566
|
tools.getSubTestPlan = server.tool("getSubTestPlan", "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs." +
|
|
564
567
|
NEEDS_PROJECT_ID +
|
|
565
|
-
NEEDS_TEST_PLAN_ID
|
|
568
|
+
NEEDS_TEST_PLAN_ID +
|
|
569
|
+
PLAN_WRITES_VIA_AGENT, GetSubTestPlanSchema.shape, {
|
|
566
570
|
title: "Get Sub Test Plan",
|
|
567
571
|
readOnlyHint: true,
|
|
568
572
|
openWorldHint: false,
|
|
@@ -23,8 +23,33 @@
|
|
|
23
23
|
export declare const NEEDS_PROJECT_ID: string;
|
|
24
24
|
/** A sibling tool can produce the id — prefer it over the agent. */
|
|
25
25
|
export declare function needsIdFrom(idLabel: string, sourceTool: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* createProjectOrFolder must NOT carry NEEDS_PROJECT_ID: `project_identifier` is optional
|
|
28
|
+
* there, and the create-a-PROJECT half needs no id at all. With the generic constant the
|
|
29
|
+
* tool read "Requires a project identifier ... call askBrowserStackAI", which routed
|
|
30
|
+
* "create me a project" through the agent before letting the tool run.
|
|
31
|
+
*/
|
|
32
|
+
export declare const PROJECT_ID_ONLY_FOR_FOLDER: string;
|
|
26
33
|
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
27
34
|
export declare const NEEDS_TEST_PLAN_ID: string;
|
|
35
|
+
/**
|
|
36
|
+
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
37
|
+
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
38
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
|
|
39
|
+
* (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
|
|
40
|
+
* /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
|
|
41
|
+
* read tools, finds no create, and reports the capability as absent — which is exactly what
|
|
42
|
+
* a QA eval concluded.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
45
|
+
* inviting the model to route plan work to the agent generally, because the tool
|
|
46
|
+
* descriptions otherwise say to prefer a specific tool whenever one fits.
|
|
47
|
+
*
|
|
48
|
+
* Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
|
|
49
|
+
* only completes on a client that can show a prompt. On one that cannot, the intended write
|
|
50
|
+
* comes back in `needs_approval` instead of happening.
|
|
51
|
+
*/
|
|
52
|
+
export declare const PLAN_WRITES_VIA_AGENT: string;
|
|
28
53
|
/** A build id comes from either build-lookup tool. */
|
|
29
54
|
export declare const NEEDS_BUILD_ID: string;
|
|
30
55
|
/** Session ids are not listable by any tool here. */
|
|
@@ -27,8 +27,37 @@ export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool
|
|
|
27
27
|
export function needsIdFrom(idLabel, sourceTool) {
|
|
28
28
|
return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* createProjectOrFolder must NOT carry NEEDS_PROJECT_ID: `project_identifier` is optional
|
|
32
|
+
* there, and the create-a-PROJECT half needs no id at all. With the generic constant the
|
|
33
|
+
* tool read "Requires a project identifier ... call askBrowserStackAI", which routed
|
|
34
|
+
* "create me a project" through the agent before letting the tool run.
|
|
35
|
+
*/
|
|
36
|
+
export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier. Creating a folder inside an EXISTING project " +
|
|
37
|
+
"needs that project's identifier (PR-*); no tool here lists projects, so ask " +
|
|
38
|
+
'askBrowserStackAI with product "tm" for it.';
|
|
30
39
|
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
31
40
|
export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "listTestPlans");
|
|
41
|
+
/**
|
|
42
|
+
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
43
|
+
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
44
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
|
|
45
|
+
* (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
|
|
46
|
+
* /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
|
|
47
|
+
* read tools, finds no create, and reports the capability as absent — which is exactly what
|
|
48
|
+
* a QA eval concluded.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
51
|
+
* inviting the model to route plan work to the agent generally, because the tool
|
|
52
|
+
* descriptions otherwise say to prefer a specific tool whenever one fits.
|
|
53
|
+
*
|
|
54
|
+
* Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
|
|
55
|
+
* only completes on a client that can show a prompt. On one that cannot, the intended write
|
|
56
|
+
* comes back in `needs_approval` instead of happening.
|
|
57
|
+
*/
|
|
58
|
+
export const PLAN_WRITES_VIA_AGENT = " Creating a test plan or sub-plan, and linking or unlinking test runs on one, are not " +
|
|
59
|
+
'available as tools here: call askBrowserStackAI with product "tm" and describe what you ' +
|
|
60
|
+
"want. It asks you to confirm before changing anything.";
|
|
32
61
|
/** A build id comes from either build-lookup tool. */
|
|
33
62
|
export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
|
|
34
63
|
/** Session ids are not listable by any tool here. */
|
|
@@ -36,12 +65,11 @@ export const NEEDS_SESSION_ID = " Requires a session id, which no tool here list
|
|
|
36
65
|
"getBuildId or listBuildId; if you have neither, call askBrowserStackAI with product " +
|
|
37
66
|
'"tra" and describe the run you mean.';
|
|
38
67
|
/** A completed scan's ids come from startAccessibilityScan, or from the agent. */
|
|
39
|
-
export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan
|
|
40
|
-
|
|
41
|
-
"
|
|
68
|
+
export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan, which are returned by startAccessibilityScan. " +
|
|
69
|
+
"No tool here lists past scans, so if you do not have the ids, start a new scan rather " +
|
|
70
|
+
"than guessing.";
|
|
42
71
|
/** Auth-config ids are not listable by any tool here. */
|
|
43
72
|
export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
|
|
44
|
-
"existing configurations, so if you do not have the id,
|
|
45
|
-
'product "a11y".';
|
|
73
|
+
"existing configurations, so if you do not have the id, create one rather than guessing.";
|
|
46
74
|
/** Test ids come from listTestIds, which itself needs a build id. */
|
|
47
75
|
export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");
|