@browserstack/mcp-server 1.5.0-beta.21 → 1.5.0-beta.23

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.
Files changed (95) hide show
  1. package/README.md +32 -48
  2. package/capability/loadtesting.capability-index.json +1876 -0
  3. package/capability/tm.capability-index.json +50104 -0
  4. package/dist/config.d.ts +1 -4
  5. package/dist/config.js +2 -23
  6. package/dist/index.d.ts +1 -2
  7. package/dist/index.js +3 -7
  8. package/dist/lib/constants.d.ts +0 -3
  9. package/dist/lib/constants.js +0 -3
  10. package/dist/lib/instrumentation.d.ts +2 -14
  11. package/dist/lib/instrumentation.js +29 -104
  12. package/dist/lib/upload-validator.d.ts +2 -3
  13. package/dist/lib/upload-validator.js +15 -18
  14. package/dist/lib/utils.js +3 -10
  15. package/dist/lib/version-resolver.d.ts +5 -0
  16. package/dist/lib/version-resolver.js +2 -8
  17. package/dist/logger.js +10 -1
  18. package/dist/server-factory.js +6 -10
  19. package/dist/tools/accessibility.js +5 -9
  20. package/dist/tools/accessiblity-utils/accessibility-rag.js +1 -3
  21. package/dist/tools/appautomate-utils/native-execution/constants.js +2 -4
  22. package/dist/tools/appautomate.js +1 -3
  23. package/dist/tools/applive.js +1 -1
  24. package/dist/tools/automate.d.ts +0 -7
  25. package/dist/tools/automate.js +1 -104
  26. package/dist/tools/bstack-sdk.js +2 -2
  27. package/dist/tools/build-insights.js +2 -41
  28. package/dist/tools/capability-registry/bind.d.ts +29 -0
  29. package/dist/tools/capability-registry/bind.js +329 -0
  30. package/dist/tools/capability-registry/config.d.ts +62 -0
  31. package/dist/tools/capability-registry/config.js +218 -0
  32. package/dist/tools/capability-registry/discovery.d.ts +44 -0
  33. package/dist/tools/capability-registry/discovery.js +99 -0
  34. package/dist/tools/capability-registry/egress.d.ts +44 -0
  35. package/dist/tools/capability-registry/egress.js +174 -0
  36. package/dist/tools/capability-registry/index-loader.d.ts +133 -0
  37. package/dist/tools/capability-registry/index-loader.js +404 -0
  38. package/dist/tools/capability-registry/register.d.ts +48 -0
  39. package/dist/tools/capability-registry/register.js +604 -0
  40. package/dist/tools/capability-registry/resolve.d.ts +38 -0
  41. package/dist/tools/capability-registry/resolve.js +45 -0
  42. package/dist/tools/capability-registry/search.d.ts +170 -0
  43. package/dist/tools/capability-registry/search.js +709 -0
  44. package/dist/tools/capability-registry/types.d.ts +282 -0
  45. package/dist/tools/capability-registry/types.js +33 -0
  46. package/dist/tools/failurelogs-utils/app-automate.js +3 -4
  47. package/dist/tools/failurelogs-utils/automate.js +4 -5
  48. package/dist/tools/get-failure-logs.js +11 -30
  49. package/dist/tools/observability.js +1 -3
  50. package/dist/tools/percy-sdk.js +9 -9
  51. package/dist/tools/rca-agent-utils/format-rca.js +4 -5
  52. package/dist/tools/rca-agent-utils/get-failed-test-id.js +0 -12
  53. package/dist/tools/rca-agent-utils/types.d.ts +0 -1
  54. package/dist/tools/rca-agent.js +2 -5
  55. package/dist/tools/review-agent.js +1 -2
  56. package/dist/tools/sdk-utils/common/constants.d.ts +1 -1
  57. package/dist/tools/sdk-utils/common/constants.js +1 -2
  58. package/dist/tools/selfheal.js +3 -6
  59. package/dist/tools/testmanagement-utils/TCG-utils/api.js +7 -26
  60. package/dist/tools/testmanagement-utils/TCG-utils/config.d.ts +0 -2
  61. package/dist/tools/testmanagement-utils/TCG-utils/config.js +0 -5
  62. package/dist/tools/testmanagement-utils/testcase-from-file.js +1 -2
  63. package/dist/tools/testmanagement-utils/upload-file.js +2 -16
  64. package/dist/tools/testmanagement.js +17 -39
  65. package/package.json +11 -5
  66. package/dist/lib/node-version-notice.d.ts +0 -1
  67. package/dist/lib/node-version-notice.js +0 -9
  68. package/dist/lib/tool-latency.d.ts +0 -4
  69. package/dist/lib/tool-latency.js +0 -16
  70. package/dist/lib/untrusted-content.d.ts +0 -15
  71. package/dist/lib/untrusted-content.js +0 -24
  72. package/dist/tools/ask-browserstack/central-oauth.d.ts +0 -120
  73. package/dist/tools/ask-browserstack/central-oauth.js +0 -277
  74. package/dist/tools/ask-browserstack/config.d.ts +0 -102
  75. package/dist/tools/ask-browserstack/config.js +0 -140
  76. package/dist/tools/ask-browserstack/egress.d.ts +0 -34
  77. package/dist/tools/ask-browserstack/egress.js +0 -31
  78. package/dist/tools/ask-browserstack/register.d.ts +0 -61
  79. package/dist/tools/ask-browserstack/register.js +0 -416
  80. package/dist/tools/ask-browserstack/relay.d.ts +0 -201
  81. package/dist/tools/ask-browserstack/relay.js +0 -577
  82. package/dist/tools/ask-browserstack/stream.d.ts +0 -116
  83. package/dist/tools/ask-browserstack/stream.js +0 -236
  84. package/dist/tools/ask-browserstack/types.d.ts +0 -196
  85. package/dist/tools/ask-browserstack/types.js +0 -14
  86. package/dist/tools/automate-utils/list-session-ids.d.ts +0 -28
  87. package/dist/tools/automate-utils/list-session-ids.js +0 -87
  88. package/dist/tools/automate-utils/resolve-hashed-build-id.d.ts +0 -30
  89. package/dist/tools/automate-utils/resolve-hashed-build-id.js +0 -124
  90. package/dist/tools/failurelogs-utils/resolve-app-build-id.d.ts +0 -2
  91. package/dist/tools/failurelogs-utils/resolve-app-build-id.js +0 -5
  92. package/dist/tools/failurelogs-utils/video.d.ts +0 -3
  93. package/dist/tools/failurelogs-utils/video.js +0 -25
  94. package/dist/tools/tool-handoff.d.ts +0 -66
  95. package/dist/tools/tool-handoff.js +0 -78
@@ -0,0 +1,282 @@
1
+ /**
2
+ * The shape of the index artifact, which is the contract with the export pipeline.
3
+ *
4
+ * The artifact is generated by the capability-registry build and transformed into its
5
+ * released envelope by the export pipeline's merge step. It contains ONLY data that has
6
+ * already passed that side's outbound boundary. Nothing here is parsed from an OpenAPI spec
7
+ * at runtime: if this half parsed specs it would become the boundary, and every gate (route
8
+ * lint, vocabulary rule, intent lint, discovery denylist) would have to be reimplemented and
9
+ * re-tested here. Reading a pre-projected index means the internal data is not in the
10
+ * package at all.
11
+ *
12
+ * ONE FILE PER PRODUCT. The released artifact is `capability/<product>.capability-index.json`,
13
+ * with the
14
+ * product object at the top level under its own name. The plural `products` wrapper belonged
15
+ * to the pre-release shape and implied multi-product files that never existed.
16
+ */
17
+ /**
18
+ * The envelope's format marker, for FUTURE shape changes.
19
+ *
20
+ * BOTH the released and the pre-release shapes claim 1, so this number cannot distinguish
21
+ * them — `readIndexFile` shape-detects on the `products` wrapper instead. The check is still
22
+ * worth keeping: it is what refuses a shape the generator has announced but this build does
23
+ * not read.
24
+ */
25
+ export declare const SUPPORTED_SCHEMA_VERSION = 1;
26
+ /** Envelope keys, i.e. everything at the top level that is NOT the product object. */
27
+ export declare const ENVELOPE_KEYS: readonly ["schema_version", "version", "build_id", "harness_commit", "products"];
28
+ export type Mode = "read" | "write" | "destructive";
29
+ /** One parameter, under the name the OpenAPI spec itself gives it. */
30
+ /**
31
+ * The constraints a parameter may declare beyond its type.
32
+ *
33
+ * ALL OPTIONAL, and absent means unchecked. The released artifact carries none of these
34
+ * yet — tm's spec holds 59 `format`, 50 `default`, 23 `minimum`, 12 `maximum`, 10
35
+ * `minItems`, 7 `maxItems`, 4 `minLength`, 3 `pattern` and 2 `maxLength` that the export
36
+ * currently drops — so this ships inert and starts working the moment the export carries
37
+ * them. An older index must keep binding exactly as it does today.
38
+ */
39
+ export interface WireConstraints {
40
+ /** Numbers. `multipleOf` is checked on the coerced number, not the raw string. */
41
+ minimum?: number;
42
+ maximum?: number;
43
+ multipleOf?: number;
44
+ /** Strings. `pattern` is an unanchored regex, compiled with `u` for `\p{…}` classes. */
45
+ minLength?: number;
46
+ maxLength?: number;
47
+ pattern?: string;
48
+ format?: string;
49
+ /** Arrays. */
50
+ minItems?: number;
51
+ maxItems?: number;
52
+ uniqueItems?: boolean;
53
+ /**
54
+ * The value the product uses when the field is omitted.
55
+ *
56
+ * PUBLISHED, NOT INJECTED. Telling a caller the default is what stops the redundant send;
57
+ * filling it in here would put a value on the wire that the caller never chose, and pin
58
+ * a server-side default that is free to change. Omission and "explicitly the default" are
59
+ * different requests, and only the product knows whether that difference matters.
60
+ */
61
+ default?: unknown;
62
+ }
63
+ /** One field inside an array item or a nested object — same constraints, one level down. */
64
+ export interface WireField extends WireConstraints {
65
+ name: string;
66
+ type: string;
67
+ required?: true;
68
+ values?: unknown[];
69
+ }
70
+ export interface WireParam extends WireConstraints {
71
+ name: string;
72
+ type: string;
73
+ required?: true;
74
+ values?: unknown[];
75
+ example?: unknown;
76
+ description?: string;
77
+ /** Field names/types one level inside an array item or nested object. */
78
+ fields?: WireField[];
79
+ /**
80
+ * Where a body field sits in the JSON, when that differs from its name. Published
81
+ * because the nesting is not guessable and getting it wrong fails silently — tm's folder
82
+ * create really wants `{folder: {name}}` while the spec's flat `{name}` is what a reader
83
+ * would assume.
84
+ */
85
+ json_path?: string;
86
+ }
87
+ /**
88
+ * A reference into a product-level lookup table.
89
+ *
90
+ * Named components are stored ONCE, already dereferenced and allOf-flattened at build time,
91
+ * so resolution is a single dict lookup rather than a recursive $ref walk at runtime.
92
+ */
93
+ export interface ComponentRef {
94
+ $response?: string;
95
+ $schema?: string;
96
+ }
97
+ /** A JSON Schema fragment, or a reference to a named one. */
98
+ export type SchemaNode = ComponentRef & Record<string, unknown>;
99
+ /** One declared response, or a reference to a named one. */
100
+ export interface ResponseDoc extends ComponentRef {
101
+ description?: string;
102
+ schema?: SchemaNode;
103
+ }
104
+ /** A capability, keyed by the endpoint it exposes. There is deliberately no name. */
105
+ export interface Capability {
106
+ /**
107
+ * The caller-facing handle — the snake-cased `operationId`, unique within its product.
108
+ *
109
+ * This is what `invokeCapability` takes, and what `entities[*].capabilities[]` lists. It
110
+ * is stable where a route is not: `/edit` becoming `/edit-v2` must not break a caller
111
+ * holding a handle.
112
+ *
113
+ * OPTIONAL because not every product publishes it yet — loadtesting's index carries none.
114
+ * For those, the endpoint remains the handle, so nothing may assume this is present.
115
+ */
116
+ name?: string;
117
+ method: string;
118
+ path: string;
119
+ mode: Mode;
120
+ entity: string;
121
+ path_params?: WireParam[];
122
+ query?: WireParam[];
123
+ body?: WireParam[];
124
+ intent?: string;
125
+ /**
126
+ * How to call the endpoint correctly.
127
+ *
128
+ * ABSENT from the released artifact — the export dropped it, and the shape-change note
129
+ * does not say whether that was intended. Kept optional and still scored by search so the
130
+ * server works either way; nothing may depend on it being present.
131
+ */
132
+ guidance?: string[];
133
+ /** Allowlisted row fields. Absent when `shape` is "discovered". */
134
+ returns?: string[];
135
+ /** "discovered" when the product declares no response schema for this operation. */
136
+ shape?: "discovered";
137
+ requires?: string[];
138
+ paginated?: boolean;
139
+ /** The largest page the operation declares. */
140
+ max_page_size?: number;
141
+ /**
142
+ * Declared responses by status code, values possibly `{$response: "Name"}` references.
143
+ *
144
+ * ADDITIVE and not yet emitted: no capability in the current export carries it. Absence
145
+ * means "no response schema available", never an error. Resolve with `resolveComponent`.
146
+ */
147
+ responses?: Record<string, ResponseDoc>;
148
+ }
149
+ export interface EntityDoc {
150
+ title?: string;
151
+ /**
152
+ * One line saying what this entity IS. Rides on `listProducts`, next to the aliases.
153
+ *
154
+ * Aliases route; they do not define. `version -> history, revision, version-history`
155
+ * tells a caller which words land here and nothing about whether that is the versioning
156
+ * of a test case or of a project. Without this line the only way to find out is
157
+ * `describeEntity`, once per entity — 19 calls at ~1.4KB for tm, against ~1.6KB to
158
+ * answer it for all of them at once.
159
+ *
160
+ * Capped at 140 characters by the build, and dropped rather than truncated when longer.
161
+ * Often absent: a product that has not authored these emits none (loadtesting has 0 of
162
+ * 9 today), and absence means "unwritten", never an error.
163
+ */
164
+ description?: string;
165
+ aliases?: string[];
166
+ id_convention?: string;
167
+ parents?: string[];
168
+ relations?: {
169
+ entity?: string;
170
+ via?: string;
171
+ }[];
172
+ /**
173
+ * What a caller gets wrong about this ENTITY, as opposed to one operation.
174
+ *
175
+ * The entity-wide half of guidance: "a run's `id` is the display string, take `uuid`"
176
+ * holds for every capability returning a run, so it is stated once here rather than
177
+ * copied onto each capability's `guidance` — which would repeat across ~20 records and,
178
+ * since guidance is a search haystack, dilute ranking with boilerplate.
179
+ *
180
+ * Filtered at build time and often absent: of tm's 155 authored facts, 73 are publishable
181
+ * as written and the rest name routes, HTTP verbs or wire-shaped parameters. Absent means
182
+ * "none passed", never an error.
183
+ */
184
+ key_facts?: string[];
185
+ [key: string]: unknown;
186
+ }
187
+ /**
188
+ * Paging controls, keyed "METHOD /path".
189
+ *
190
+ * NOT USED BY THIS SERVER: it performs one request and returns the response, so paging
191
+ * belongs to the caller and the page parameters are published with the endpoint's other
192
+ * query parameters. The field is still emitted by the build, so it stays described here
193
+ * rather than silently ignored — a consumer that DOES page can use it.
194
+ */
195
+ export interface PagingRule {
196
+ page?: string;
197
+ size?: string;
198
+ /** The largest page the operation declares. Absent when the spec states no maximum. */
199
+ max?: number;
200
+ }
201
+ /**
202
+ * How a product wants the caller's credentials presented.
203
+ *
204
+ * The OpenAPI `securityScheme` vocabulary, verbatim — `type` / `in` / `name` / `scheme` —
205
+ * because the spec already describes this and inventing a parallel taxonomy would mean
206
+ * translating between two of them forever. ONE scheme per product, not OpenAPI's list of
207
+ * alternatives: this server holds a username and an access key and nothing else, so
208
+ * "the first alternative we can satisfy" only ever had one answer.
209
+ *
210
+ * `template` is what makes it a contract rather than a guess — a product taking
211
+ * `{username}_{access_key}` is expressible instead of being a special case in the server.
212
+ * It names placeholders, never values: the credential itself stays in config, and the
213
+ * harness's `{ env: … }` value sources must NOT cross the boundary into the artifact.
214
+ */
215
+ export interface AuthScheme {
216
+ /** "apiKey" puts the rendered template in a header; "http" with scheme "basic" encodes it. */
217
+ type: "apiKey" | "http";
218
+ /** apiKey only. Header is the sole supported location — see `egress.authHeaders`. */
219
+ in?: "header" | "cookie" | "query";
220
+ /** apiKey only: the header name, e.g. "Api-Token". */
221
+ name?: string;
222
+ /** http only. */
223
+ scheme?: string;
224
+ /** Defaults to `{username}:{access_key}`. */
225
+ template?: string;
226
+ }
227
+ export interface ProductIndex {
228
+ summary: string;
229
+ /**
230
+ * How to authenticate to this product. Absent means the historical default: the caller's
231
+ * credentials as `Api-Token: {username}:{access_key}`, which is what every shipped index
232
+ * relies on today.
233
+ */
234
+ auth?: AuthScheme;
235
+ /**
236
+ * The single host this product is served from, when it has one.
237
+ *
238
+ * A default, not the last word: config overrides it, and for a region-sharded product
239
+ * (one declaring `base_urls`) account discovery outranks it and this is the fallback.
240
+ */
241
+ base_url?: string;
242
+ /**
243
+ * Candidate regional hosts, in probe order, for a product whose host depends on the
244
+ * ACCOUNT rather than the deployment.
245
+ *
246
+ * Declaring these makes the product region-sharded: the server asks each in turn with the
247
+ * caller's credentials and keeps the one that answers. A single fixed `base_url` cannot
248
+ * express this — it would send every account outside the default region to the wrong host,
249
+ * a failure invisible to anyone testing from inside that region.
250
+ */
251
+ base_urls?: string[];
252
+ /**
253
+ * The endpoint to probe the candidates with. Derived from the capabilities when absent
254
+ * (see `discovery.probePath`); declare it when the derived choice would be wrong.
255
+ */
256
+ probe_path?: string;
257
+ capabilities: Capability[];
258
+ entities: Record<string, EntityDoc>;
259
+ paging?: Record<string, PagingRule>;
260
+ /** Named responses, referenced as `{$response: "Name"}`. Additive; not yet emitted. */
261
+ responses?: Record<string, ResponseDoc>;
262
+ /** Named schemas, referenced as `{$schema: "Name"}`. Additive; not yet emitted. */
263
+ schemas?: Record<string, SchemaNode>;
264
+ }
265
+ /** Where one product's data came from. Logging and cache-busting only. */
266
+ export interface Provenance {
267
+ /** `<commit>_<UTC timestamp>` in the released shape. */
268
+ build_id: string;
269
+ /** Dotted content-generation counter, e.g. "1.2". Absent in the pre-release shape. */
270
+ version?: string;
271
+ }
272
+ /**
273
+ * The merged, in-memory model: every loaded file's product under its own name.
274
+ *
275
+ * This is the server's own structure, not the file format — one file carries one product,
276
+ * and the registry holds all of them so a caller can search across products.
277
+ */
278
+ export interface RegistryIndex {
279
+ schema_version: number;
280
+ build_id: string;
281
+ products: Record<string, ProductIndex>;
282
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The shape of the index artifact, which is the contract with the export pipeline.
3
+ *
4
+ * The artifact is generated by the capability-registry build and transformed into its
5
+ * released envelope by the export pipeline's merge step. It contains ONLY data that has
6
+ * already passed that side's outbound boundary. Nothing here is parsed from an OpenAPI spec
7
+ * at runtime: if this half parsed specs it would become the boundary, and every gate (route
8
+ * lint, vocabulary rule, intent lint, discovery denylist) would have to be reimplemented and
9
+ * re-tested here. Reading a pre-projected index means the internal data is not in the
10
+ * package at all.
11
+ *
12
+ * ONE FILE PER PRODUCT. The released artifact is `capability/<product>.capability-index.json`,
13
+ * with the
14
+ * product object at the top level under its own name. The plural `products` wrapper belonged
15
+ * to the pre-release shape and implied multi-product files that never existed.
16
+ */
17
+ /**
18
+ * The envelope's format marker, for FUTURE shape changes.
19
+ *
20
+ * BOTH the released and the pre-release shapes claim 1, so this number cannot distinguish
21
+ * them — `readIndexFile` shape-detects on the `products` wrapper instead. The check is still
22
+ * worth keeping: it is what refuses a shape the generator has announced but this build does
23
+ * not read.
24
+ */
25
+ export const SUPPORTED_SCHEMA_VERSION = 1;
26
+ /** Envelope keys, i.e. everything at the top level that is NOT the product object. */
27
+ export const ENVELOPE_KEYS = [
28
+ "schema_version",
29
+ "version",
30
+ "build_id",
31
+ "harness_commit",
32
+ "products",
33
+ ];
@@ -1,7 +1,6 @@
1
1
  import { getBrowserStackAuth } from "../../lib/get-auth.js";
2
2
  import { filterLinesByKeywords, validateLogResponse } from "./utils.js";
3
3
  import { apiClient } from "../../lib/apiClient.js";
4
- import { wrapUntrusted } from "../../lib/untrusted-content.js";
5
4
  // DEVICE LOGS
6
5
  export async function retrieveDeviceLogs(sessionId, buildId, config) {
7
6
  const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/deviceLogs`;
@@ -23,7 +22,7 @@ export async function retrieveDeviceLogs(sessionId, buildId, config) {
23
22
  : JSON.stringify(response.data);
24
23
  const logs = filterDeviceFailures(logText);
25
24
  return logs.length > 0
26
- ? `Device Failures (${logs.length} found):\n${wrapUntrusted("device logs", JSON.stringify(logs, null, 2))}`
25
+ ? `Device Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
27
26
  : "No device failures found";
28
27
  }
29
28
  // APPIUM LOGS
@@ -47,7 +46,7 @@ export async function retrieveAppiumLogs(sessionId, buildId, config) {
47
46
  : JSON.stringify(response.data);
48
47
  const logs = filterAppiumFailures(logText);
49
48
  return logs.length > 0
50
- ? `Appium Failures (${logs.length} found):\n${wrapUntrusted("Appium logs", JSON.stringify(logs, null, 2))}`
49
+ ? `Appium Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
51
50
  : "No Appium failures found";
52
51
  }
53
52
  // CRASH LOGS
@@ -71,7 +70,7 @@ export async function retrieveCrashLogs(sessionId, buildId, config) {
71
70
  : JSON.stringify(response.data);
72
71
  const logs = filterCrashFailures(logText);
73
72
  return logs.length > 0
74
- ? `Crash Failures (${logs.length} found):\n${wrapUntrusted("crash logs", JSON.stringify(logs, null, 2))}`
73
+ ? `Crash Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
75
74
  : "No crash failures found";
76
75
  }
77
76
  // FILTER HELPERS
@@ -1,5 +1,4 @@
1
1
  import { getBrowserStackAuth } from "../../lib/get-auth.js";
2
- import { wrapUntrusted } from "../../lib/untrusted-content.js";
3
2
  import { filterLinesByKeywords, validateLogResponse, } from "./utils.js";
4
3
  import { apiClient } from "../../lib/apiClient.js";
5
4
  // NETWORK LOGS
@@ -23,7 +22,7 @@ export async function retrieveNetworkFailures(sessionId, config) {
23
22
  entry.response.status >= 400 ||
24
23
  entry.response._error !== undefined);
25
24
  return failureEntries.length > 0
26
- ? `Network Failures (${failureEntries.length} found):\n${wrapUntrusted("network logs", JSON.stringify(failureEntries.map((entry) => ({
25
+ ? `Network Failures (${failureEntries.length} found):\n${JSON.stringify(failureEntries.map((entry) => ({
27
26
  startedDateTime: entry.startedDateTime,
28
27
  request: {
29
28
  method: entry.request?.method,
@@ -37,7 +36,7 @@ export async function retrieveNetworkFailures(sessionId, config) {
37
36
  },
38
37
  serverIPAddress: entry.serverIPAddress,
39
38
  time: entry.time,
40
- })), null, 2))}`
39
+ })), null, 2)}`
41
40
  : "No network failures found";
42
41
  }
43
42
  // SESSION LOGS
@@ -61,7 +60,7 @@ export async function retrieveSessionFailures(sessionId, config) {
61
60
  : JSON.stringify(response.data);
62
61
  const logs = filterSessionFailures(logText);
63
62
  return logs.length > 0
64
- ? `Session Failures (${logs.length} found):\n${wrapUntrusted("session logs", JSON.stringify(logs, null, 2))}`
63
+ ? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
65
64
  : "No session failures found";
66
65
  }
67
66
  // CONSOLE LOGS
@@ -85,7 +84,7 @@ export async function retrieveConsoleFailures(sessionId, config) {
85
84
  : JSON.stringify(response.data);
86
85
  const logs = filterConsoleFailures(logText);
87
86
  return logs.length > 0
88
- ? `Console Failures (${logs.length} found):\n${wrapUntrusted("console logs", JSON.stringify(logs, null, 2))}`
87
+ ? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
89
88
  : "No console failures found";
90
89
  }
91
90
  // FILTER: session logs
@@ -1,11 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { trackMCP } from "../lib/instrumentation.js";
3
- import { NEEDS_SESSION_ID } from "./tool-handoff.js";
4
3
  import { retrieveNetworkFailures, retrieveSessionFailures, retrieveConsoleFailures, } from "./failurelogs-utils/automate.js";
5
4
  import { retrieveDeviceLogs, retrieveAppiumLogs, retrieveCrashLogs, } from "./failurelogs-utils/app-automate.js";
6
- import { resolveAppAutomateBuildId } from "./failurelogs-utils/resolve-app-build-id.js";
7
- import { retrieveSessionVideo } from "./failurelogs-utils/video.js";
8
- import { AppAutomateLogType, AutomateLogType, SessionType, SessionVideoLogType, } from "../lib/constants.js";
5
+ import { AppAutomateLogType, AutomateLogType, SessionType, } from "../lib/constants.js";
9
6
  // Main log fetcher function
10
7
  export async function getFailureLogs(args, config) {
11
8
  const results = [];
@@ -14,16 +11,17 @@ export async function getFailureLogs(args, config) {
14
11
  if (!args.sessionId) {
15
12
  throw new Error("Session ID is required");
16
13
  }
14
+ if (args.sessionType === SessionType.AppAutomate && !args.buildId) {
15
+ throw new Error("Build ID is required for app-automate sessions");
16
+ }
17
17
  // Validate log types and collect errors
18
18
  validLogTypes = args.logTypes.filter((logType) => {
19
19
  const isAutomate = Object.values(AutomateLogType).includes(logType);
20
20
  const isAppAutomate = Object.values(AppAutomateLogType).includes(logType);
21
21
  if (!isAutomate && !isAppAutomate) {
22
22
  errors.push(`Invalid log type '${logType}'. Valid log types are: ${[
23
- ...new Set([
24
- ...Object.values(AutomateLogType),
25
- ...Object.values(AppAutomateLogType),
26
- ]),
23
+ ...Object.values(AutomateLogType),
24
+ ...Object.values(AppAutomateLogType),
27
25
  ].join(", ")}`);
28
26
  return false;
29
27
  }
@@ -48,26 +46,11 @@ export async function getFailureLogs(args, config) {
48
46
  isError: true,
49
47
  };
50
48
  }
51
- let buildId = args.buildId;
52
- const needsBuildId = validLogTypes.some((logType) => logType !== SessionVideoLogType);
53
- if (args.sessionType === SessionType.AppAutomate &&
54
- needsBuildId &&
55
- !buildId) {
56
- buildId = await resolveAppAutomateBuildId(args.sessionId, config);
57
- if (!buildId) {
58
- throw new Error("Build ID is required for app-automate sessions");
59
- }
60
- }
61
49
  let response;
62
50
  // eslint-disable-next-line no-useless-catch
63
51
  try {
64
52
  for (const logType of validLogTypes) {
65
53
  switch (logType) {
66
- case SessionVideoLogType: {
67
- response = await retrieveSessionVideo(args.sessionId, args.sessionType, config);
68
- results.push({ type: "text", text: response });
69
- break;
70
- }
71
54
  case AutomateLogType.NetworkLogs: {
72
55
  response = await retrieveNetworkFailures(args.sessionId, config);
73
56
  results.push({ type: "text", text: response });
@@ -84,17 +67,17 @@ export async function getFailureLogs(args, config) {
84
67
  break;
85
68
  }
86
69
  case AppAutomateLogType.DeviceLogs: {
87
- response = await retrieveDeviceLogs(args.sessionId, buildId, config);
70
+ response = await retrieveDeviceLogs(args.sessionId, args.buildId, config);
88
71
  results.push({ type: "text", text: response });
89
72
  break;
90
73
  }
91
74
  case AppAutomateLogType.AppiumLogs: {
92
- response = await retrieveAppiumLogs(args.sessionId, buildId, config);
75
+ response = await retrieveAppiumLogs(args.sessionId, args.buildId, config);
93
76
  results.push({ type: "text", text: response });
94
77
  break;
95
78
  }
96
79
  case AppAutomateLogType.CrashLogs: {
97
- response = await retrieveCrashLogs(args.sessionId, buildId, config);
80
+ response = await retrieveCrashLogs(args.sessionId, args.buildId, config);
98
81
  results.push({ type: "text", text: response });
99
82
  break;
100
83
  }
@@ -115,8 +98,7 @@ export async function getFailureLogs(args, config) {
115
98
  // Register tool with the MCP server
116
99
  export default function registerGetFailureLogs(server, config) {
117
100
  const tools = {};
118
- tools.getFailureLogs = server.tool("getFailureLogs", "Fetch logs, or the session video URL, for an Automate/App Automate session." +
119
- NEEDS_SESSION_ID, {
101
+ tools.getFailureLogs = server.tool("getFailureLogs", "Fetch various types of logs from a BrowserStack session. Supports both automate and app-automate sessions.", {
120
102
  sessionType: z
121
103
  .enum([SessionType.Automate, SessionType.AppAutomate])
122
104
  .describe("Type of BrowserStack session. Must be explicitly provided by the user."),
@@ -126,7 +108,7 @@ export default function registerGetFailureLogs(server, config) {
126
108
  buildId: z
127
109
  .string()
128
110
  .optional()
129
- .describe("App Automate build ID. Optional — resolved from the session if omitted."),
111
+ .describe("Required only when sessionType is 'app-automate'. If sessionType is 'app-automate', always ask the user to provide the build ID before proceeding."),
130
112
  logTypes: z
131
113
  .array(z.enum([
132
114
  AutomateLogType.NetworkLogs,
@@ -135,7 +117,6 @@ export default function registerGetFailureLogs(server, config) {
135
117
  AppAutomateLogType.DeviceLogs,
136
118
  AppAutomateLogType.AppiumLogs,
137
119
  AppAutomateLogType.CrashLogs,
138
- SessionVideoLogType,
139
120
  ]))
140
121
  .describe("The types of logs to fetch."),
141
122
  }, {
@@ -1,4 +1,3 @@
1
- import { wrapUntrusted } from "../lib/untrusted-content.js";
2
1
  import { z } from "zod";
3
2
  import { getLatestO11YBuildInfo } from "../lib/api.js";
4
3
  import { trackMCP } from "../lib/instrumentation.js";
@@ -27,8 +26,7 @@ export async function getFailuresInLastRun(buildName, projectName, config) {
27
26
  content: [
28
27
  {
29
28
  type: "text",
30
- text: `Observability URL: ${observabilityUrl}\n` +
31
- wrapUntrusted("observability failure report", `Overview: ${overview}\nError Details: ${details}`),
29
+ text: `Observability URL: ${observabilityUrl}\nOverview: ${overview}\nError Details: ${details}`,
32
30
  },
33
31
  ],
34
32
  };
@@ -38,11 +38,11 @@ export function registerPercyTools(server, config) {
38
38
  idempotentHint: true,
39
39
  }, async (args) => {
40
40
  try {
41
- trackMCP("percyVisualTestIntegrationAgent", server.server.getClientVersion(), undefined, config);
41
+ trackMCP("VisualTestIntegrationAgent", server.server.getClientVersion(), config);
42
42
  return simulatePercyChangeHandler(args, config);
43
43
  }
44
44
  catch (error) {
45
- return handleMCPError("percyVisualTestIntegrationAgent", server, config, error);
45
+ return handleMCPError("VisualTestIntegrationAgent", server, config, error);
46
46
  }
47
47
  });
48
48
  tools.setupPercyVisualTesting = server.tool("expandPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, {
@@ -53,11 +53,11 @@ export function registerPercyTools(server, config) {
53
53
  idempotentHint: true,
54
54
  }, async (args) => {
55
55
  try {
56
- trackMCP("expandPercyVisualTesting", server.server.getClientVersion(), undefined, config);
56
+ trackMCP("setupPercyVisualTesting", server.server.getClientVersion(), config);
57
57
  return setUpPercyHandler(args, config);
58
58
  }
59
59
  catch (error) {
60
- return handleMCPError("expandPercyVisualTesting", server, config, error);
60
+ return handleMCPError("setupPercyVisualTesting", server, config, error);
61
61
  }
62
62
  });
63
63
  tools.addPercySnapshotCommands = server.tool("addPercySnapshotCommands", PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, UpdateTestFileWithInstructionsParams, {
@@ -68,7 +68,7 @@ export function registerPercyTools(server, config) {
68
68
  idempotentHint: true,
69
69
  }, async (args) => {
70
70
  try {
71
- trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), undefined, config);
71
+ trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), config);
72
72
  return await updateTestsWithPercyCommands(args);
73
73
  }
74
74
  catch (error) {
@@ -83,7 +83,7 @@ export function registerPercyTools(server, config) {
83
83
  idempotentHint: true,
84
84
  }, async () => {
85
85
  try {
86
- trackMCP("listTestFiles", server.server.getClientVersion(), undefined, config);
86
+ trackMCP("listTestFiles", server.server.getClientVersion(), config);
87
87
  return addListTestFiles();
88
88
  }
89
89
  catch (error) {
@@ -98,7 +98,7 @@ export function registerPercyTools(server, config) {
98
98
  idempotentHint: true,
99
99
  }, async (args) => {
100
100
  try {
101
- trackMCP("runPercyScan", server.server.getClientVersion(), undefined, config);
101
+ trackMCP("runPercyScan", server.server.getClientVersion(), config);
102
102
  return runPercyScan(args);
103
103
  }
104
104
  catch (error) {
@@ -113,7 +113,7 @@ export function registerPercyTools(server, config) {
113
113
  idempotentHint: true,
114
114
  }, async (args) => {
115
115
  try {
116
- trackMCP("fetchPercyChanges", server.server.getClientVersion(), undefined, config);
116
+ trackMCP("fetchPercyChanges", server.server.getClientVersion(), config);
117
117
  return await fetchPercyChanges(args, config);
118
118
  }
119
119
  catch (error) {
@@ -128,7 +128,7 @@ export function registerPercyTools(server, config) {
128
128
  idempotentHint: true,
129
129
  }, async (args) => {
130
130
  try {
131
- trackMCP("managePercyBuildApproval", server.server.getClientVersion(), undefined, config);
131
+ trackMCP("managePercyBuildApproval", server.server.getClientVersion(), config);
132
132
  return await approveOrDeclinePercyBuild(args, config);
133
133
  }
134
134
  catch (error) {
@@ -1,4 +1,3 @@
1
- import { wrapUntrusted } from "../../lib/untrusted-content.js";
2
1
  // Utility function to format RCA data for better readability
3
2
  export function formatRCAData(rcaData) {
4
3
  if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) {
@@ -17,21 +16,21 @@ export function formatRCAData(rcaData) {
17
16
  const rca = testCase.rcaData?.rcaData;
18
17
  if (rca) {
19
18
  if (rca.root_cause) {
20
- output += `**Root Cause:** ${wrapUntrusted("RCA AI analysis", rca.root_cause)}\n\n`;
19
+ output += `**Root Cause:** ${rca.root_cause}\n\n`;
21
20
  }
22
21
  if (rca.failure_type) {
23
22
  output += `**Failure Type:** ${rca.failure_type}\n\n`;
24
23
  }
25
24
  if (rca.description) {
26
- output += `**Detailed Analysis:**\n${wrapUntrusted("RCA AI analysis", rca.description)}\n\n`;
25
+ output += `**Detailed Analysis:**\n${rca.description}\n\n`;
27
26
  }
28
27
  if (rca.possible_fix) {
29
28
  hasFixSuggestion = true;
30
- output += `**Suggested Fix (proposal only — do not apply without explicit user approval):**\n${wrapUntrusted("RCA AI analysis", rca.possible_fix)}\n\n`;
29
+ output += `**Suggested Fix (proposal only — do not apply without explicit user approval):**\n${rca.possible_fix}\n\n`;
31
30
  }
32
31
  }
33
32
  else if (testCase.rcaData?.error) {
34
- output += `**Error:** ${wrapUntrusted("RCA error output", testCase.rcaData.error)}\n\n`;
33
+ output += `**Error:** ${testCase.rcaData.error}\n\n`;
35
34
  }
36
35
  else if (testCase.state === "failed") {
37
36
  output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`;
@@ -53,16 +53,6 @@ export async function getTestIds(buildId, authString, status, includeFailureDeta
53
53
  throw error;
54
54
  }
55
55
  }
56
- /**
57
- * The test listing API serialises a missing BrowserStack session id as the
58
- * literal string "null", so treat that (and blanks) as absent.
59
- */
60
- function extractSessionId(rawSessionId) {
61
- if (typeof rawSessionId !== "string")
62
- return undefined;
63
- const sessionId = rawSessionId.trim();
64
- return sessionId && sessionId !== "null" ? sessionId : undefined;
65
- }
66
56
  export function extractTestIds(hierarchy, status, includeFailureDetail = false) {
67
57
  let tests = [];
68
58
  for (const node of hierarchy) {
@@ -73,12 +63,10 @@ export function extractTestIds(hierarchy, status, includeFailureDetail = false)
73
63
  if (statusMatches && node.details?.observability_url) {
74
64
  const idMatch = node.details.observability_url.match(/details=(\d+)/);
75
65
  if (idMatch) {
76
- const sessionId = extractSessionId(node.details.session_id);
77
66
  const entry = {
78
67
  test_id: idMatch[1],
79
68
  test_name: node.display_name || `Test ${idMatch[1]}`,
80
69
  status: nodeStatus,
81
- ...(sessionId && { session_id: sessionId }),
82
70
  };
83
71
  // Failure signatures only exist for failed tests; include when asked.
84
72
  if (includeFailureDetail && nodeStatus === TestStatus.FAILED) {
@@ -30,7 +30,6 @@ export interface FailedTestInfo {
30
30
  test_name: string;
31
31
  status?: TestStatus;
32
32
  failure?: TestFailureSignature;
33
- session_id?: string;
34
33
  }
35
34
  export declare enum RCAState {
36
35
  PENDING = "pending",