@browserstack/mcp-server 1.2.33-beta.2 → 1.2.34-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/elicit-credentials.d.ts +21 -0
- package/dist/lib/elicit-credentials.js +66 -0
- package/dist/tools/accessibility.js +36 -3
- package/dist/tools/testmanagement-utils/create-lca-steps.d.ts +1 -0
- package/dist/tools/testmanagement-utils/create-lca-steps.js +6 -1
- package/dist/tools/testmanagement.js +41 -1
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export interface CredentialField {
|
|
3
|
+
key: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Collects credential values for a tool WITHOUT routing them through the model.
|
|
9
|
+
*
|
|
10
|
+
* When the connected client advertises MCP elicitation support, any requested
|
|
11
|
+
* field the caller did not already supply is requested directly from the user
|
|
12
|
+
* via the client — the value flows user -> client -> server and never appears in
|
|
13
|
+
* the LLM's tool-call arguments, context, or logs.
|
|
14
|
+
*
|
|
15
|
+
* When the client does not support elicitation, or the user declines/cancels, or
|
|
16
|
+
* the request errors, the values are returned exactly as provided. This keeps the
|
|
17
|
+
* existing argument-based flow working unchanged (backward compatible), and makes
|
|
18
|
+
* the helper safe to ship to transports that cannot elicit (it degrades to the
|
|
19
|
+
* arg path rather than failing).
|
|
20
|
+
*/
|
|
21
|
+
export declare function elicitCredentialsIfSupported(server: McpServer, provided: Record<string, string | undefined>, fields: CredentialField[], message: string): Promise<Record<string, string | undefined>>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import logger from "../logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* Collects credential values for a tool WITHOUT routing them through the model.
|
|
4
|
+
*
|
|
5
|
+
* When the connected client advertises MCP elicitation support, any requested
|
|
6
|
+
* field the caller did not already supply is requested directly from the user
|
|
7
|
+
* via the client — the value flows user -> client -> server and never appears in
|
|
8
|
+
* the LLM's tool-call arguments, context, or logs.
|
|
9
|
+
*
|
|
10
|
+
* When the client does not support elicitation, or the user declines/cancels, or
|
|
11
|
+
* the request errors, the values are returned exactly as provided. This keeps the
|
|
12
|
+
* existing argument-based flow working unchanged (backward compatible), and makes
|
|
13
|
+
* the helper safe to ship to transports that cannot elicit (it degrades to the
|
|
14
|
+
* arg path rather than failing).
|
|
15
|
+
*/
|
|
16
|
+
export async function elicitCredentialsIfSupported(server, provided, fields, message) {
|
|
17
|
+
const missing = fields.filter((field) => !provided[field.key]);
|
|
18
|
+
if (missing.length === 0) {
|
|
19
|
+
return provided;
|
|
20
|
+
}
|
|
21
|
+
// Only attempt elicitation when the client explicitly supports it; otherwise
|
|
22
|
+
// fall back to the caller-provided values (existing behavior).
|
|
23
|
+
const capabilities = server.server.getClientCapabilities();
|
|
24
|
+
if (!capabilities?.elicitation) {
|
|
25
|
+
return provided;
|
|
26
|
+
}
|
|
27
|
+
const properties = {};
|
|
28
|
+
for (const field of missing) {
|
|
29
|
+
properties[field.key] = {
|
|
30
|
+
type: "string",
|
|
31
|
+
title: field.title,
|
|
32
|
+
description: field.description,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
let result;
|
|
36
|
+
try {
|
|
37
|
+
result = await server.server.elicitInput({
|
|
38
|
+
mode: "form",
|
|
39
|
+
message,
|
|
40
|
+
requestedSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties,
|
|
43
|
+
required: missing.map((field) => field.key),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A client that advertised elicitation but failed to handle it must not
|
|
49
|
+
// break the tool — fall back to whatever was provided. The error is not
|
|
50
|
+
// logged: a client validation error can echo the submitted form values
|
|
51
|
+
// (including the password), which must never reach the logs.
|
|
52
|
+
logger.warn("Elicitation request failed; falling back to provided values.");
|
|
53
|
+
return provided;
|
|
54
|
+
}
|
|
55
|
+
if (result.action !== "accept" || !result.content) {
|
|
56
|
+
return provided;
|
|
57
|
+
}
|
|
58
|
+
const merged = { ...provided };
|
|
59
|
+
for (const field of missing) {
|
|
60
|
+
const value = result.content[field.key];
|
|
61
|
+
if (typeof value === "string" && value.length > 0) {
|
|
62
|
+
merged[field.key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
@@ -6,6 +6,7 @@ import { trackMCP } from "../lib/instrumentation.js";
|
|
|
6
6
|
import { parseAccessibilityReportFromCSV } from "./accessiblity-utils/report-parser.js";
|
|
7
7
|
import { queryAccessibilityRAG } from "./accessiblity-utils/accessibility-rag.js";
|
|
8
8
|
import { getBrowserStackAuth } from "../lib/get-auth.js";
|
|
9
|
+
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
|
|
9
10
|
import logger from "../logger.js";
|
|
10
11
|
function setupAuth(config) {
|
|
11
12
|
const authString = getBrowserStackAuth(config);
|
|
@@ -235,8 +236,14 @@ export default function addAccessibilityTools(server, config) {
|
|
|
235
236
|
.enum(["form", "basic"])
|
|
236
237
|
.describe("Authentication type: 'form' for form-based auth, 'basic' for HTTP basic auth"),
|
|
237
238
|
url: z.string().describe("URL of the authentication page"),
|
|
238
|
-
username: z
|
|
239
|
-
|
|
239
|
+
username: z
|
|
240
|
+
.string()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe("Site username; omit to have it requested from the user."),
|
|
243
|
+
password: z
|
|
244
|
+
.string()
|
|
245
|
+
.optional()
|
|
246
|
+
.describe("Site password; omit to have it requested from the user."),
|
|
240
247
|
usernameSelector: z
|
|
241
248
|
.string()
|
|
242
249
|
.optional()
|
|
@@ -250,7 +257,33 @@ export default function addAccessibilityTools(server, config) {
|
|
|
250
257
|
.optional()
|
|
251
258
|
.describe("CSS selector for submit button (required for form auth)"),
|
|
252
259
|
}, async (args) => {
|
|
253
|
-
|
|
260
|
+
try {
|
|
261
|
+
const creds = await elicitCredentialsIfSupported(server, { username: args.username, password: args.password }, [
|
|
262
|
+
{
|
|
263
|
+
key: "username",
|
|
264
|
+
title: "Site username",
|
|
265
|
+
description: `Username for the login being configured ("${args.name}")`,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: "password",
|
|
269
|
+
title: "Site password",
|
|
270
|
+
description: `Password for the login being configured ("${args.name}")`,
|
|
271
|
+
},
|
|
272
|
+
], `Enter the login credentials for accessibility auth config "${args.name}".`);
|
|
273
|
+
if (!creds.username || !creds.password) {
|
|
274
|
+
const error = new Error("Username and password are required to create an auth config. Provide them when prompted, or pass them as arguments.");
|
|
275
|
+
trackMCP("createAccessibilityAuthConfig", server.server.getClientVersion(), error, config);
|
|
276
|
+
return createErrorResponse(error.message);
|
|
277
|
+
}
|
|
278
|
+
return await executeCreateAuthConfig({
|
|
279
|
+
...args,
|
|
280
|
+
username: creds.username,
|
|
281
|
+
password: creds.password,
|
|
282
|
+
}, server, config);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
return handleMCPError("createAccessibilityAuthConfig", server, config, error);
|
|
286
|
+
}
|
|
254
287
|
});
|
|
255
288
|
tools.getAccessibilityAuthConfig = server.tool("getAccessibilityAuthConfig", "Retrieve an existing authentication configuration by ID.", {
|
|
256
289
|
configId: z.number().describe("ID of the auth configuration to retrieve"),
|
|
@@ -8,6 +8,7 @@ export declare const CreateLCAStepsSchema: z.ZodObject<{
|
|
|
8
8
|
project_identifier: z.ZodString;
|
|
9
9
|
test_case_identifier: z.ZodString;
|
|
10
10
|
base_url: z.ZodString;
|
|
11
|
+
requires_authentication: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
11
12
|
credentials: z.ZodOptional<z.ZodObject<{
|
|
12
13
|
username: z.ZodString;
|
|
13
14
|
password: z.ZodString;
|
|
@@ -15,13 +15,18 @@ export const CreateLCAStepsSchema = z.object({
|
|
|
15
15
|
.string()
|
|
16
16
|
.describe("Identifier of the test case (e.g., 'TC-12345')"),
|
|
17
17
|
base_url: z.string().describe("Base URL for the test (e.g., 'google.com')"),
|
|
18
|
+
requires_authentication: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe("Set true if the test case requires login."),
|
|
18
23
|
credentials: z
|
|
19
24
|
.object({
|
|
20
25
|
username: z.string().describe("Username for authentication"),
|
|
21
26
|
password: z.string().describe("Password for authentication"),
|
|
22
27
|
})
|
|
23
28
|
.optional()
|
|
24
|
-
.describe("
|
|
29
|
+
.describe("Login credentials; omit to have them requested from the user."),
|
|
25
30
|
local_enabled: z
|
|
26
31
|
.boolean()
|
|
27
32
|
.optional()
|
|
@@ -18,6 +18,7 @@ import { listTestPlans, ListTestPlansSchema, } from "./testmanagement-utils/list
|
|
|
18
18
|
import { getTestPlan, GetTestPlanSchema, } from "./testmanagement-utils/get-testplan.js";
|
|
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
|
+
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
|
|
21
22
|
//TODO: Moving the traceMCP and catch block to the parent(server) function
|
|
22
23
|
/**
|
|
23
24
|
* Wrapper to call createProjectOrFolder util.
|
|
@@ -284,7 +285,46 @@ export async function createTestCasesFromFileTool(args, context, config, server)
|
|
|
284
285
|
export async function createLCAStepsTool(args, context, config, server) {
|
|
285
286
|
try {
|
|
286
287
|
trackMCP("createLCASteps", server.server.getClientVersion(), undefined, config);
|
|
287
|
-
|
|
288
|
+
let effectiveArgs = args;
|
|
289
|
+
if (args.requires_authentication &&
|
|
290
|
+
(!args.credentials?.username || !args.credentials?.password)) {
|
|
291
|
+
const creds = await elicitCredentialsIfSupported(server, {
|
|
292
|
+
username: args.credentials?.username,
|
|
293
|
+
password: args.credentials?.password,
|
|
294
|
+
}, [
|
|
295
|
+
{
|
|
296
|
+
key: "username",
|
|
297
|
+
title: "Login username",
|
|
298
|
+
description: `Username for test case ${args.test_case_identifier}`,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
key: "password",
|
|
302
|
+
title: "Login password",
|
|
303
|
+
description: `Password for test case ${args.test_case_identifier}`,
|
|
304
|
+
},
|
|
305
|
+
], `Enter the login credentials for test case ${args.test_case_identifier}.`);
|
|
306
|
+
if (creds.username && creds.password) {
|
|
307
|
+
effectiveArgs = {
|
|
308
|
+
...args,
|
|
309
|
+
credentials: { username: creds.username, password: creds.password },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// requires_authentication was requested but no credentials could be
|
|
314
|
+
// obtained (client can't elicit, or the user declined). Fail clearly
|
|
315
|
+
// rather than creating a login test case with no credentials.
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: "text",
|
|
320
|
+
text: `Authentication is required for test case ${args.test_case_identifier}, but no credentials were provided. Provide them when prompted, or pass them as arguments.`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
isError: true,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return await createLCASteps(effectiveArgs, context, config);
|
|
288
328
|
}
|
|
289
329
|
catch (err) {
|
|
290
330
|
trackMCP("createLCASteps", server.server.getClientVersion(), err, config);
|