@browserstack/mcp-server 1.3.0 → 1.3.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.
@@ -0,0 +1,131 @@
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { getBrowserStackAuth } from "../../lib/get-auth.js";
3
+ import { getO11yBaseUrl, getRcaViewGuidance, RCA_CHAT_POLL_PATH, } from "./constants.js";
4
+ import { PENDING_STATUS, TfaStatus, } from "./types.js";
5
+ export class TfaRcaTurnError extends Error {
6
+ }
7
+ export function buildAuthHeader(config) {
8
+ const authString = getBrowserStackAuth(config);
9
+ return `Basic ${Buffer.from(authString).toString("base64")}`;
10
+ }
11
+ /** Map a raw status string from the wire onto the `TfaStatus` enum. */
12
+ function toTfaStatus(raw) {
13
+ switch (raw) {
14
+ case "RESOLVED":
15
+ return TfaStatus.RESOLVED;
16
+ case "BLOCKED":
17
+ return TfaStatus.BLOCKED;
18
+ default:
19
+ return TfaStatus.NEEDS_INFO;
20
+ }
21
+ }
22
+ /** Map one wire ask (snake_case) to the client `TfaAsk` (camelCase). */
23
+ function toAsk(raw) {
24
+ return {
25
+ what: raw?.what ?? "",
26
+ why: raw?.why ?? "",
27
+ evidenceType: raw?.evidence_type ?? "other",
28
+ priority: raw?.priority ?? "medium",
29
+ };
30
+ }
31
+ // The poll envelope's `status` is the lifecycle state (working/completed/failed);
32
+ // the agent's TurnResponse under `turn` has its own status (NEEDS_INFO/RESOLVED/
33
+ // BLOCKED). Default-fills lists so the skill never sees `undefined`.
34
+ export function readStructuredTurn(data) {
35
+ const turn = data.turn ?? {};
36
+ const status = toTfaStatus(turn.status);
37
+ const needsInfo = turn.needs_info ?? {};
38
+ const blocked = turn.blocked ?? {};
39
+ return {
40
+ status,
41
+ confidence: turn.confidence ?? "unknown",
42
+ questions: Array.isArray(needsInfo.questions) ? needsInfo.questions : [],
43
+ asks: Array.isArray(needsInfo.asks) ? needsInfo.asks.map(toAsk) : [],
44
+ suggestions: Array.isArray(needsInfo.suggestions)
45
+ ? needsInfo.suggestions
46
+ : [],
47
+ hypotheses: Array.isArray(needsInfo.hypotheses) ? needsInfo.hypotheses : [],
48
+ rca: status === TfaStatus.RESOLVED ? (turn.rca ?? undefined) : undefined,
49
+ reason: status === TfaStatus.BLOCKED ? blocked.reason : undefined,
50
+ unmetAsks: status === TfaStatus.BLOCKED && Array.isArray(blocked.unmet_asks)
51
+ ? blocked.unmet_asks
52
+ : undefined,
53
+ };
54
+ }
55
+ // Trim a completed turn to the status-discriminated contract: NEEDS_INFO
56
+ // carries questions/asks/suggestions/hypotheses verbatim; RESOLVED carries a
57
+ // glimpse + viewRca pointer; BLOCKED carries reason + unmetAsks.
58
+ export function toTrimmedResult(turn, threadId) {
59
+ switch (turn.status) {
60
+ case TfaStatus.RESOLVED: {
61
+ const rca = turn.rca ?? {};
62
+ return {
63
+ status: turn.status,
64
+ confidence: turn.confidence,
65
+ threadId,
66
+ glimpse: {
67
+ root_cause: rca.root_cause,
68
+ failure_type: rca.failure_type,
69
+ related_prs: rca.related_prs,
70
+ },
71
+ viewRca: getRcaViewGuidance(),
72
+ };
73
+ }
74
+ case TfaStatus.BLOCKED:
75
+ return {
76
+ status: turn.status,
77
+ confidence: turn.confidence,
78
+ threadId,
79
+ reason: turn.reason,
80
+ unmetAsks: turn.unmetAsks,
81
+ };
82
+ default:
83
+ return {
84
+ status: turn.status,
85
+ confidence: turn.confidence,
86
+ threadId,
87
+ questions: turn.questions,
88
+ asks: turn.asks,
89
+ suggestions: turn.suggestions,
90
+ hypotheses: turn.hypotheses,
91
+ };
92
+ }
93
+ }
94
+ /** Build the poll (GET) URL for one already-submitted turn. */
95
+ export function buildPollUrl(testRunId, turnId) {
96
+ return (getO11yBaseUrl() +
97
+ RCA_CHAT_POLL_PATH.replace("{testRunId}", testRunId).replace("{turnId}", turnId));
98
+ }
99
+ // Read an already-submitted turn once (no polling loop, no submit); a turn
100
+ // still in flight yields the soft PENDING status carrying turnId/threadId.
101
+ export async function getTfaTurnResult(args, config) {
102
+ const headers = {
103
+ "Content-Type": "application/json",
104
+ Authorization: buildAuthHeader(config),
105
+ };
106
+ const response = await apiClient.get({
107
+ url: buildPollUrl(args.testRunId, args.turnId),
108
+ headers,
109
+ raise_error: false,
110
+ });
111
+ if (response.status === 404) {
112
+ throw new TfaRcaTurnError("turn expired or not found");
113
+ }
114
+ if (!response.ok) {
115
+ throw new TfaRcaTurnError(`failed to read RCA turn (status ${response.status})`);
116
+ }
117
+ const data = response.data ?? {};
118
+ const threadId = data.threadId;
119
+ if (data.status === "failed") {
120
+ throw new TfaRcaTurnError(data.error || "TFA agent run failed");
121
+ }
122
+ if (data.status === "completed") {
123
+ return toTrimmedResult(readStructuredTurn(data), threadId);
124
+ }
125
+ // "working" (or any other in-progress value) → soft PENDING, read again later.
126
+ return {
127
+ status: PENDING_STATUS,
128
+ threadId,
129
+ turnId: args.turnId,
130
+ };
131
+ }
@@ -0,0 +1,61 @@
1
+ export declare enum TfaStatus {
2
+ NEEDS_INFO = "NEEDS_INFO",
3
+ RESOLVED = "RESOLVED",
4
+ BLOCKED = "BLOCKED"
5
+ }
6
+ export declare const PENDING_STATUS: "PENDING";
7
+ export type Confidence = "low" | "medium" | "high" | "unknown";
8
+ export type EvidenceType = "test_logs" | "product_code" | "k8s" | "kibana" | "metrics" | "deploy" | "ci" | "other";
9
+ /** A typed request for evidence; the skill routes it by `evidenceType`. */
10
+ export interface TfaAsk {
11
+ what: string;
12
+ why: string;
13
+ evidenceType: EvidenceType;
14
+ priority: "high" | "medium" | "low";
15
+ }
16
+ export interface TfaRca {
17
+ root_cause?: string;
18
+ description?: string;
19
+ possible_fix?: string;
20
+ failure_type?: string;
21
+ alternatives_considered?: string[];
22
+ related_prs?: unknown[];
23
+ [key: string]: unknown;
24
+ }
25
+ export interface TurnResponse {
26
+ status: TfaStatus;
27
+ confidence: Confidence;
28
+ questions: string[];
29
+ asks: TfaAsk[];
30
+ suggestions: string[];
31
+ hypotheses: string[];
32
+ rca?: TfaRca;
33
+ /** Present on BLOCKED turns: why TFA cannot proceed. */
34
+ reason?: string;
35
+ /** Present on BLOCKED turns: the asks that went unmet. */
36
+ unmetAsks?: string[];
37
+ }
38
+ export interface TfaRcaGlimpse {
39
+ /** Truncated to `RCA_GLIMPSE_ROOT_CAUSE_MAX` chars. */
40
+ root_cause?: string;
41
+ failure_type?: string;
42
+ related_prs?: unknown[];
43
+ }
44
+ export interface TfaRcaTurnResult {
45
+ status: TfaStatus | typeof PENDING_STATUS;
46
+ confidence?: Confidence;
47
+ threadId?: string;
48
+ /** Present on PENDING turns: resume via the tool's `turnId` arg. */
49
+ turnId?: string;
50
+ questions?: string[];
51
+ asks?: TfaAsk[];
52
+ suggestions?: string[];
53
+ hypotheses?: string[];
54
+ /** Present on RESOLVED turns: trimmed root-cause glimpse. */
55
+ glimpse?: TfaRcaGlimpse;
56
+ rca?: unknown;
57
+ /** Present on RESOLVED turns: where the full RCA report lives. */
58
+ viewRca?: string;
59
+ reason?: string;
60
+ unmetAsks?: string[];
61
+ }
@@ -0,0 +1,9 @@
1
+ // Collaboration status emitted by the TFA agent for one RCA turn.
2
+ export var TfaStatus;
3
+ (function (TfaStatus) {
4
+ TfaStatus["NEEDS_INFO"] = "NEEDS_INFO";
5
+ TfaStatus["RESOLVED"] = "RESOLVED";
6
+ TfaStatus["BLOCKED"] = "BLOCKED";
7
+ })(TfaStatus || (TfaStatus = {}));
8
+ // Soft, tool-emitted status when an in-call poll exceeds its wall-clock cap.
9
+ export const PENDING_STATUS = "PENDING";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",