@browserstack/mcp-server 1.2.18 → 1.2.20-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/apiClient.js +0 -2
- package/dist/tools/applive-utils/start-session.js +32 -22
- package/dist/tools/live-utils/start-session.js +37 -20
- package/dist/tools/testmanagement-utils/TCG-utils/api.d.ts +9 -0
- package/dist/tools/testmanagement-utils/TCG-utils/api.js +14 -0
- package/dist/tools/testmanagement-utils/create-testcase.d.ts +2 -0
- package/dist/tools/testmanagement-utils/create-testcase.js +26 -2
- package/dist/tools/testmanagement-utils/update-testcase.js +1 -21
- package/package.json +1 -1
package/dist/lib/apiClient.js
CHANGED
|
@@ -55,7 +55,6 @@ function getAxiosAgent() {
|
|
|
55
55
|
host: proxyHost,
|
|
56
56
|
port: Number(proxyPort),
|
|
57
57
|
ca,
|
|
58
|
-
rejectUnauthorized: false, // Set to true if you want strict SSL
|
|
59
58
|
});
|
|
60
59
|
}
|
|
61
60
|
else {
|
|
@@ -67,7 +66,6 @@ function getAxiosAgent() {
|
|
|
67
66
|
// CA only
|
|
68
67
|
return new https.Agent({
|
|
69
68
|
ca: fs.readFileSync(caCertPath),
|
|
70
|
-
rejectUnauthorized: false, // Set to true for strict SSL
|
|
71
69
|
});
|
|
72
70
|
}
|
|
73
71
|
// Default agent (no proxy, no CA)
|
|
@@ -5,7 +5,6 @@ import { uploadApp } from "./upload-app.js";
|
|
|
5
5
|
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
6
6
|
import { findDeviceByName } from "./device-search.js";
|
|
7
7
|
import { pickVersion } from "./version-utils.js";
|
|
8
|
-
import childProcess from "child_process";
|
|
9
8
|
import envConfig from "../../config.js";
|
|
10
9
|
/**
|
|
11
10
|
* Start an App Live session: filter, select, upload, and open.
|
|
@@ -69,33 +68,44 @@ export async function startSession(args, options) {
|
|
|
69
68
|
});
|
|
70
69
|
const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;
|
|
71
70
|
if (!envConfig.REMOTE_MCP) {
|
|
72
|
-
|
|
71
|
+
const openCommand = getOpenBrowserCommand(launchUrl);
|
|
72
|
+
if (openCommand) {
|
|
73
|
+
return [
|
|
74
|
+
`App Live session URL: ${launchUrl}${note}`,
|
|
75
|
+
``,
|
|
76
|
+
`To open the session in the default browser, run:`,
|
|
77
|
+
` ${openCommand}`,
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
73
80
|
}
|
|
74
81
|
return launchUrl + note;
|
|
75
82
|
}
|
|
76
83
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
84
|
+
* Returns the platform-appropriate shell command to open `launchUrl` in the
|
|
85
|
+
* default browser, or null if the URL is not a trusted BrowserStack URL.
|
|
86
|
+
*
|
|
87
|
+
* The command is returned to the MCP client so the host agent can prompt the
|
|
88
|
+
* user before executing it. The server itself never spawns a process, which
|
|
89
|
+
* eliminates the command-injection surface entirely.
|
|
80
90
|
*/
|
|
81
|
-
function
|
|
91
|
+
function getOpenBrowserCommand(launchUrl) {
|
|
92
|
+
let parsed;
|
|
82
93
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
|
|
89
|
-
const child = childProcess.spawn(command[0], command.slice(1), {
|
|
90
|
-
stdio: "ignore",
|
|
91
|
-
detached: true,
|
|
92
|
-
});
|
|
93
|
-
child.on("error", (error) => {
|
|
94
|
-
logger.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
95
|
-
});
|
|
96
|
-
child.unref();
|
|
94
|
+
parsed = new URL(launchUrl);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
logger.error(`Refusing to surface malformed URL: ${launchUrl}`);
|
|
98
|
+
return null;
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
if (parsed.protocol !== "https:" ||
|
|
101
|
+
!/(^|\.)browserstack\.com$/i.test(parsed.hostname)) {
|
|
102
|
+
logger.error(`Refusing to surface untrusted URL: ${launchUrl}`);
|
|
103
|
+
return null;
|
|
100
104
|
}
|
|
105
|
+
const quoted = `"${parsed.toString()}"`;
|
|
106
|
+
if (process.platform === "darwin")
|
|
107
|
+
return `open ${quoted}`;
|
|
108
|
+
if (process.platform === "win32")
|
|
109
|
+
return `cmd /c start "" ${quoted}`;
|
|
110
|
+
return `xdg-open ${quoted}`;
|
|
101
111
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logger from "../../logger.js";
|
|
2
|
-
import childProcess from "child_process";
|
|
3
2
|
import { filterDesktop } from "./desktop-filter.js";
|
|
4
3
|
import { filterMobile } from "./mobile-filter.js";
|
|
5
4
|
import { PlatformType, } from "./types.js";
|
|
@@ -39,10 +38,19 @@ export async function startBrowserSession(args, config) {
|
|
|
39
38
|
const url = args.platformType === PlatformType.DESKTOP
|
|
40
39
|
? buildDesktopUrl(args, entry, isLocal)
|
|
41
40
|
: buildMobileUrl(args, entry, isLocal);
|
|
41
|
+
const note = entry.notes ? `, ${entry.notes}` : "";
|
|
42
42
|
if (!envConfig.REMOTE_MCP) {
|
|
43
|
-
|
|
43
|
+
const openCommand = getOpenBrowserCommand(url);
|
|
44
|
+
if (openCommand) {
|
|
45
|
+
return [
|
|
46
|
+
`Live session URL: ${url}${note}`,
|
|
47
|
+
``,
|
|
48
|
+
`To open the session in the default browser, run:`,
|
|
49
|
+
` ${openCommand}`,
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
44
52
|
}
|
|
45
|
-
return
|
|
53
|
+
return `${url}${note}`;
|
|
46
54
|
}
|
|
47
55
|
function buildDesktopUrl(args, e, isLocal) {
|
|
48
56
|
const params = new URLSearchParams({
|
|
@@ -79,24 +87,33 @@ function buildMobileUrl(args, d, isLocal) {
|
|
|
79
87
|
});
|
|
80
88
|
return `https://live.browserstack.com/dashboard#${params.toString()}`;
|
|
81
89
|
}
|
|
82
|
-
// ———
|
|
83
|
-
|
|
90
|
+
// ——— Build a browser-open command for the host agent ———
|
|
91
|
+
/**
|
|
92
|
+
* Returns the platform-appropriate shell command to open `launchUrl` in the
|
|
93
|
+
* default browser, or null if the URL is not a trusted BrowserStack URL.
|
|
94
|
+
*
|
|
95
|
+
* The command is returned to the MCP client so the host agent can prompt the
|
|
96
|
+
* user before executing it. The server itself never spawns a process, which
|
|
97
|
+
* eliminates the command-injection surface entirely.
|
|
98
|
+
*/
|
|
99
|
+
function getOpenBrowserCommand(launchUrl) {
|
|
100
|
+
let parsed;
|
|
84
101
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
|
|
91
|
-
const child = childProcess.spawn(command[0], command.slice(1), {
|
|
92
|
-
stdio: "ignore",
|
|
93
|
-
detached: true,
|
|
94
|
-
...(process.platform === "win32" ? { shell: true } : {}),
|
|
95
|
-
});
|
|
96
|
-
child.on("error", (err) => logger.error(`Failed to open browser: ${err}. URL: ${launchUrl}`));
|
|
97
|
-
child.unref();
|
|
102
|
+
parsed = new URL(launchUrl);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
logger.error(`Refusing to surface malformed URL: ${launchUrl}`);
|
|
106
|
+
return null;
|
|
98
107
|
}
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
if (parsed.protocol !== "https:" ||
|
|
109
|
+
!/(^|\.)browserstack\.com$/i.test(parsed.hostname)) {
|
|
110
|
+
logger.error(`Refusing to surface untrusted URL: ${launchUrl}`);
|
|
111
|
+
return null;
|
|
101
112
|
}
|
|
113
|
+
const quoted = `"${parsed.toString()}"`;
|
|
114
|
+
if (process.platform === "darwin")
|
|
115
|
+
return `open ${quoted}`;
|
|
116
|
+
if (process.platform === "win32")
|
|
117
|
+
return `cmd /c start "" ${quoted}`;
|
|
118
|
+
return `xdg-open ${quoted}`;
|
|
102
119
|
}
|
|
@@ -7,6 +7,15 @@ export declare function fetchFormFields(projectId: string, config: BrowserStackC
|
|
|
7
7
|
default_fields: any;
|
|
8
8
|
custom_fields: any;
|
|
9
9
|
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a default-field input (priority/case_type) to the form's display or
|
|
12
|
+
* internal name, matching case-insensitively. Returns undefined if no match.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeDefaultFieldValue(fieldValues: Array<{
|
|
15
|
+
internal_name?: string | null;
|
|
16
|
+
name?: string;
|
|
17
|
+
value: any;
|
|
18
|
+
}>, input: string, emit: "name" | "internal_name"): string | undefined;
|
|
10
19
|
/**
|
|
11
20
|
* Trigger AI-based test case generation for a document.
|
|
12
21
|
*/
|
|
@@ -16,6 +16,20 @@ export async function fetchFormFields(projectId, config) {
|
|
|
16
16
|
});
|
|
17
17
|
return res.data;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a default-field input (priority/case_type) to the form's display or
|
|
21
|
+
* internal name, matching case-insensitively. Returns undefined if no match.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeDefaultFieldValue(fieldValues, input, emit) {
|
|
24
|
+
const normalized = input.toLowerCase().trim();
|
|
25
|
+
const match = fieldValues.find((v) => (v.internal_name ?? "").toLowerCase() === normalized ||
|
|
26
|
+
(v.name ?? "").toLowerCase() === normalized);
|
|
27
|
+
if (!match)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (emit === "name")
|
|
30
|
+
return match.name;
|
|
31
|
+
return match.internal_name ?? match.name;
|
|
32
|
+
}
|
|
19
33
|
/**
|
|
20
34
|
* Trigger AI-based test case generation for a document.
|
|
21
35
|
*/
|
|
@@ -22,6 +22,7 @@ export interface TestCaseCreateRequest {
|
|
|
22
22
|
tags?: string[];
|
|
23
23
|
custom_fields?: Record<string, string>;
|
|
24
24
|
automation_status?: string;
|
|
25
|
+
priority?: string;
|
|
25
26
|
}
|
|
26
27
|
export interface TestCaseResponse {
|
|
27
28
|
data: {
|
|
@@ -70,6 +71,7 @@ export declare const CreateTestCaseSchema: z.ZodObject<{
|
|
|
70
71
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
71
72
|
custom_fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
72
73
|
automation_status: z.ZodOptional<z.ZodString>;
|
|
74
|
+
priority: z.ZodOptional<z.ZodString>;
|
|
73
75
|
}, z.core.$strip>;
|
|
74
76
|
export declare function sanitizeArgs(args: any): any;
|
|
75
77
|
export declare function createTestCase(params: TestCaseCreateRequest, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { apiClient } from "../../lib/apiClient.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { formatAxiosError } from "../../lib/error.js";
|
|
4
|
-
import { projectIdentifierToId } from "./TCG-utils/api.js";
|
|
4
|
+
import { fetchFormFields, normalizeDefaultFieldValue, projectIdentifierToId, } from "./TCG-utils/api.js";
|
|
5
5
|
import { getTMBaseURL } from "../../lib/tm-base-url.js";
|
|
6
|
+
import logger from "../../logger.js";
|
|
6
7
|
export const CreateTestCaseSchema = z.object({
|
|
7
8
|
project_identifier: z
|
|
8
9
|
.string()
|
|
@@ -54,6 +55,10 @@ export const CreateTestCaseSchema = z.object({
|
|
|
54
55
|
.string()
|
|
55
56
|
.optional()
|
|
56
57
|
.describe("Automation status of the test case. Common values include 'not_automated', 'automated', 'automation_not_required'."),
|
|
58
|
+
priority: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Priority of the test case. Accepts either display name (e.g. 'Critical', 'High', 'Medium', 'Low') or internal name (e.g. 'medium'). If omitted, the project default (usually 'Medium') is applied. Valid values are per-project and discoverable via the form-fields endpoint."),
|
|
57
62
|
});
|
|
58
63
|
export function sanitizeArgs(args) {
|
|
59
64
|
const cleaned = { ...args };
|
|
@@ -74,8 +79,27 @@ export function sanitizeArgs(args) {
|
|
|
74
79
|
return cleaned;
|
|
75
80
|
}
|
|
76
81
|
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
82
|
+
/**
|
|
83
|
+
* Normalize priority to the display name the create endpoint accepts (it
|
|
84
|
+
* rejects lowercase). On lookup failure, pass the raw value through.
|
|
85
|
+
*/
|
|
86
|
+
async function normalizePriority(projectIdentifier, priority, config) {
|
|
87
|
+
try {
|
|
88
|
+
const numericProjectId = await projectIdentifierToId(projectIdentifier, config);
|
|
89
|
+
const { default_fields } = await fetchFormFields(numericProjectId, config);
|
|
90
|
+
return (normalizeDefaultFieldValue(default_fields?.priority?.values ?? [], priority, "name") ?? priority);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
logger.warn("Failed to normalize priority value; passing through as given: %s", err instanceof Error ? err.message : String(err));
|
|
94
|
+
return priority;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
77
97
|
export async function createTestCase(params, config) {
|
|
78
|
-
const
|
|
98
|
+
const testCaseParams = { ...params };
|
|
99
|
+
if (testCaseParams.priority !== undefined) {
|
|
100
|
+
testCaseParams.priority = await normalizePriority(params.project_identifier, testCaseParams.priority, config);
|
|
101
|
+
}
|
|
102
|
+
const body = { test_case: testCaseParams };
|
|
79
103
|
const authString = getBrowserStackAuth(config);
|
|
80
104
|
const [username, password] = authString.split(":");
|
|
81
105
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { apiClient } from "../../lib/apiClient.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { formatAxiosError } from "../../lib/error.js";
|
|
4
|
-
import { fetchFormFields, projectIdentifierToId } from "./TCG-utils/api.js";
|
|
4
|
+
import { fetchFormFields, normalizeDefaultFieldValue, projectIdentifierToId, } from "./TCG-utils/api.js";
|
|
5
5
|
import { getTMBaseURL } from "../../lib/tm-base-url.js";
|
|
6
6
|
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
7
7
|
import logger from "../../logger.js";
|
|
@@ -62,26 +62,6 @@ export const UpdateTestCaseSchema = z.object({
|
|
|
62
62
|
.optional()
|
|
63
63
|
.describe("Map of custom field name/id to value. Valid field names and value types are per-project; discover them via the project's form fields."),
|
|
64
64
|
});
|
|
65
|
-
/**
|
|
66
|
-
* Build a normalizer for a default field's accepted value.
|
|
67
|
-
* The TM PATCH endpoint accepts different casings for different default
|
|
68
|
-
* fields (Title-Case display name for priority/case_type, snake_case
|
|
69
|
-
* internal_name for automation_status). We accept either from the caller
|
|
70
|
-
* and emit the form the API actually wants.
|
|
71
|
-
*
|
|
72
|
-
* Returns undefined when no matching option is found — callers should
|
|
73
|
-
* pass the raw value through so the backend can surface its own error.
|
|
74
|
-
*/
|
|
75
|
-
function normalizeDefaultFieldValue(fieldValues, input, emit) {
|
|
76
|
-
const normalized = input.toLowerCase().trim();
|
|
77
|
-
const match = fieldValues.find((v) => (v.internal_name ?? "").toLowerCase() === normalized ||
|
|
78
|
-
(v.name ?? "").toLowerCase() === normalized);
|
|
79
|
-
if (!match)
|
|
80
|
-
return undefined;
|
|
81
|
-
if (emit === "name")
|
|
82
|
-
return match.name;
|
|
83
|
-
return match.internal_name ?? match.name;
|
|
84
|
-
}
|
|
85
65
|
/**
|
|
86
66
|
* Normalise default-field inputs (priority/case_type/automation_status) to
|
|
87
67
|
* what the TM PATCH endpoint accepts. Fetches the project's form-fields
|