@browserstack/mcp-server 1.2.33 → 1.2.34

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.
@@ -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.string().describe("Username for authentication"),
239
- password: z.string().describe("Password for authentication"),
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
- return await executeCreateAuthConfig(args, server, config);
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("Optional credentials for authentication. Extract from the test case details if provided in it. This is required for the test cases which require authentication."),
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
- return await createLCASteps(args, context, config);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.2.33",
3
+ "version": "1.2.34",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",