@browserstack/mcp-server 2.1.0 → 2.2.0-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/capability/loadtesting.capability-index.json +2978 -0
- package/capability/tm.capability-index.json +2 -1
- package/capability/tra.capability-index.json +52074 -0
- package/dist/config.d.ts +2 -27
- package/dist/config.js +6 -19
- package/dist/lib/central-oauth.d.ts +133 -0
- package/dist/lib/central-oauth.js +246 -0
- package/dist/lib/utils.js +0 -8
- package/dist/tools/ask-browserstack/central-oauth.d.ts +25 -46
- package/dist/tools/ask-browserstack/central-oauth.js +55 -177
- package/dist/tools/ask-browserstack/config.d.ts +14 -0
- package/dist/tools/ask-browserstack/config.js +16 -0
- package/dist/tools/ask-browserstack/register.d.ts +2 -3
- package/dist/tools/ask-browserstack/register.js +72 -219
- package/dist/tools/ask-browserstack/relay.js +17 -11
- package/dist/tools/ask-browserstack/stream.d.ts +5 -27
- package/dist/tools/ask-browserstack/stream.js +14 -25
- package/dist/tools/ask-browserstack/types.d.ts +3 -54
- package/dist/tools/ask-browserstack/types.js +1 -11
- package/dist/tools/automate-utils/fetch-screenshots.js +15 -17
- package/dist/tools/capability-registry/bind.js +43 -7
- package/dist/tools/capability-registry/discovery.js +3 -1
- package/dist/tools/capability-registry/egress.d.ts +12 -5
- package/dist/tools/capability-registry/egress.js +24 -1
- package/dist/tools/capability-registry/oauth.d.ts +63 -0
- package/dist/tools/capability-registry/oauth.js +176 -0
- package/dist/tools/capability-registry/register.js +289 -41
- package/dist/tools/capability-registry/resolve.d.ts +1 -1
- package/dist/tools/capability-registry/resolve.js +24 -3
- package/dist/tools/capability-registry/routing-token.d.ts +25 -0
- package/dist/tools/capability-registry/routing-token.js +87 -0
- package/dist/tools/capability-registry/search.d.ts +28 -1
- package/dist/tools/capability-registry/search.js +202 -4
- package/dist/tools/capability-registry/types.d.ts +42 -3
- package/dist/tools/testmanagement.js +5 -5
- package/dist/tools/tool-handoff.d.ts +15 -12
- package/dist/tools/tool-handoff.js +21 -18
- package/package.json +2 -1
- package/dist/tools/ask-browserstack/deferred.d.ts +0 -74
- package/dist/tools/ask-browserstack/deferred.js +0 -242
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The two-call shape: ask now, decide later, with nothing held open in between.
|
|
3
|
-
*
|
|
4
|
-
* WHY IT EXISTS. Elicitation is what makes this process stateful — a suspended
|
|
5
|
-
* `elicitation/create` pins an McpServer in one pod's heap until the answer arrives, so
|
|
6
|
-
* the hosted deployment needs sessions, ingress affinity keyed on the caller's
|
|
7
|
-
* credentials, and a cap on how many sessions may be alive. Deferred removes the
|
|
8
|
-
* suspension rather than routing around it: Atlas returns at its first ask, we hand that
|
|
9
|
-
* ask back, and the caller returns with the decision in a SECOND call. Between the two
|
|
10
|
-
* this process holds nothing, so any pod can serve either.
|
|
11
|
-
*
|
|
12
|
-
* WHAT IT COSTS, stated plainly because it is not free:
|
|
13
|
-
* - Approval rides the CLIENT's own permission prompt on the second call, not an
|
|
14
|
-
* elicitation. A client configured not to prompt (`--permission-mode auto`) approves
|
|
15
|
-
* with no human. Elicitation fails closed by construction; this fails closed only if
|
|
16
|
-
* the client asks. That is the trade, and it is why stdio keeps elicitation.
|
|
17
|
-
* - One extra round trip per APPROVAL, not per task. Atlas's gate is serial, so a task
|
|
18
|
-
* with five writes is five approvals and six calls.
|
|
19
|
-
*
|
|
20
|
-
* The run itself is parked on Atlas's side for the gate's own 300s, kept alive exactly as
|
|
21
|
-
* a dropped reader leaves it (CONTRACT v2 §4.2). Nothing here has to manage that.
|
|
22
|
-
*/
|
|
23
|
-
import logger from "../../logger.js";
|
|
24
|
-
import { RELAY_CONTRACT, decisionUrl, parseAsk, } from "./stream.js";
|
|
25
|
-
import { RELAY_OFF_DETAILS, buildResult } from "./relay.js";
|
|
26
|
-
function asRecord(value) {
|
|
27
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
28
|
-
? value
|
|
29
|
-
: {};
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Atlas's `needs_approval` payload → our result, or `null` when it is not one.
|
|
33
|
-
*
|
|
34
|
-
* Null rather than a throw: a deferred request whose run never needed approval comes
|
|
35
|
-
* back as an ORDINARY result, and that is a success to pass straight through — not an
|
|
36
|
-
* unexpected shape. A read-only task must cost one call, not two.
|
|
37
|
-
*/
|
|
38
|
-
export function parkedResult(body, product) {
|
|
39
|
-
const payload = asRecord(body);
|
|
40
|
-
if (payload.status !== "needs_approval")
|
|
41
|
-
return null;
|
|
42
|
-
const runId = String(payload.run_id || "");
|
|
43
|
-
const asks = Array.isArray(payload.asks) ? payload.asks : [];
|
|
44
|
-
// Validated, not cast, for the same reason the streaming path validates: an ask with
|
|
45
|
-
// no usable `perm_id` cannot be answered, so handing it to a caller would invite a
|
|
46
|
-
// second call that can only 404.
|
|
47
|
-
const ask = asks.length ? parseAsk(asks[0]) : null;
|
|
48
|
-
if (!runId || !ask) {
|
|
49
|
-
logger.error("askBrowserStackAI: deferred run parked without an answerable ask; treating as a plain result");
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
runId,
|
|
54
|
-
ask: {
|
|
55
|
-
perm_id: ask.perm_id,
|
|
56
|
-
description: ask.description,
|
|
57
|
-
product: ask.product || product,
|
|
58
|
-
mode: ask.mode,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
/** CONTRACT §5's shape, for a run that is parked rather than finished. */
|
|
63
|
-
export function needsApprovalResult(runId, ask, product) {
|
|
64
|
-
return {
|
|
65
|
-
// NOT ok, and not an error either: nothing was applied and nothing failed. A caller
|
|
66
|
-
// that branches on `ok` alone must not read a parked run as a finished one.
|
|
67
|
-
ok: false,
|
|
68
|
-
status: "needs_approval",
|
|
69
|
-
// Addressed to the MODEL, and imperative, because the model is what reads it and
|
|
70
|
-
// the one thing that must happen next is not something it can do by itself.
|
|
71
|
-
answer: `A HUMAN MUST APPROVE THIS BEFORE IT CAN CONTINUE. BrowserStack AI stopped at: ` +
|
|
72
|
-
`${ask.description}. Nothing has been changed yet.\n\n` +
|
|
73
|
-
`Put this to the user and WAIT for their answer. If you have a way to ask them a ` +
|
|
74
|
-
`structured question or show a confirmation — any prompt, question or approval ` +
|
|
75
|
-
`affordance your client offers — use that; otherwise ask plainly in your reply. ` +
|
|
76
|
-
`Quote what is being done either way.\n\n` +
|
|
77
|
-
`Do NOT decide on their behalf, and do not treat "it is what they asked for" as ` +
|
|
78
|
-
`approval: they are being asked to confirm this specific change, which is a ` +
|
|
79
|
-
`different question. Once they have answered, follow \`next_step\`.`,
|
|
80
|
-
approvals: [],
|
|
81
|
-
approvals_source: "atlas",
|
|
82
|
-
elicitations: [],
|
|
83
|
-
needs_approval: [{ description: ask.description, mode: ask.mode }],
|
|
84
|
-
applied_before_stop: false,
|
|
85
|
-
permission_relay: {
|
|
86
|
-
used: true,
|
|
87
|
-
reason: "",
|
|
88
|
-
// Terse on purpose: the actionable half lives in `next_step`, and an explanation
|
|
89
|
-
// of the transport here is what the model read instead of the instruction.
|
|
90
|
-
detail: "Approval is pending a human answer; see `next_step`.",
|
|
91
|
-
},
|
|
92
|
-
atlas_response: null,
|
|
93
|
-
run_id: runId,
|
|
94
|
-
perm_id: ask.perm_id,
|
|
95
|
-
next_step: {
|
|
96
|
-
instruction:
|
|
97
|
-
// Deliberately names no tool and no client. The affordance differs everywhere
|
|
98
|
-
// — a structured question, a confirmation dialog, an approval prompt, or
|
|
99
|
-
// nothing at all — and naming one would be wrong on every other client and
|
|
100
|
-
// would rot as clients change. Describe the ACT, let the model pick the means.
|
|
101
|
-
"AFTER the user has answered — not before — call askBrowserStackAI again " +
|
|
102
|
-
"with exactly these params, setting `decision` to what THEY said. If they " +
|
|
103
|
-
'declined, send "deny": that is a valid answer and stops the task cleanly. ' +
|
|
104
|
-
"If they have not answered yet, do not call anything.",
|
|
105
|
-
call: "askBrowserStackAI",
|
|
106
|
-
params: {
|
|
107
|
-
product,
|
|
108
|
-
run_id: runId,
|
|
109
|
-
perm_id: ask.perm_id,
|
|
110
|
-
decision: "allow | deny — the user's answer, not your own judgement",
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
/** The FIRST call: run until the first ask, then answer. Nothing is held open. */
|
|
116
|
-
export async function runDeferred(url, headers, body, transport, mode, product) {
|
|
117
|
-
const response = await transport(url, headers, {
|
|
118
|
-
...body,
|
|
119
|
-
permission_relay: { mode: "deferred", contract: RELAY_CONTRACT },
|
|
120
|
-
});
|
|
121
|
-
const parked = parkedResult(response.body, product);
|
|
122
|
-
if (parked) {
|
|
123
|
-
logger.info("askBrowserStackAI: run parked awaiting approval (run=%s)", parked.runId);
|
|
124
|
-
return needsApprovalResult(parked.runId, parked.ask, product);
|
|
125
|
-
}
|
|
126
|
-
// No ask: an ordinary result, passed through the same builder the streaming path
|
|
127
|
-
// uses so an entitlement refusal, a 401 and a plain failure all read identically
|
|
128
|
-
// whichever transport produced them.
|
|
129
|
-
return buildResult(response, [], mode, product);
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* The SECOND call: deliver the decision, then pick the run back up.
|
|
133
|
-
*
|
|
134
|
-
* Two requests, in this order, and the order is the safety property: the decision is
|
|
135
|
-
* posted FIRST so that a reattach which then fails cannot leave the gate waiting on an
|
|
136
|
-
* answer we already have. A lost reattach costs the caller the answer; a lost decision
|
|
137
|
-
* would cost them the run.
|
|
138
|
-
*/
|
|
139
|
-
export async function runResumed(agentUrl, headers, runId, permId, decision, transport, mode, product) {
|
|
140
|
-
// ONE CALL. This used to POST the decision, check for a 204, and then reattach to
|
|
141
|
-
// `GET /agent/{run_id}/stream` to collect what the run did next — two requests, and a
|
|
142
|
-
// reattach that only existed because the run was a paused coroutine in one pod. The
|
|
143
|
-
// ask parks in Atlas's checkpoint now, so the decision request IS the one that carries
|
|
144
|
-
// the run forward and the continuation comes back in its own reply, from any replica.
|
|
145
|
-
let response;
|
|
146
|
-
try {
|
|
147
|
-
response = await transport(decisionUrl(agentUrl, runId), headers, {
|
|
148
|
-
perm_id: permId,
|
|
149
|
-
decision,
|
|
150
|
-
reason: "",
|
|
151
|
-
// Required. The run resumes on a fresh Agent that must be scoped before it can be
|
|
152
|
-
// built, and Atlas deliberately will not take that scope from persisted state.
|
|
153
|
-
product,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// CAUGHT HERE rather than left to the handler's outer catch, which cannot tell this
|
|
158
|
-
// apart from a request that never left and so reports `not_reached` — "nothing was
|
|
159
|
-
// changed" — for a decision that may well have completed.
|
|
160
|
-
logger.warn("askBrowserStackAI: no reply to the decision on run %s; outcome unknown", runId);
|
|
161
|
-
return decisionUnknown();
|
|
162
|
-
}
|
|
163
|
-
if (response.status === 404) {
|
|
164
|
-
// The only failure worth its own words: there is no such parked run. It expired, it
|
|
165
|
-
// was already answered, or this is not the caller that owns it. None of those is an
|
|
166
|
-
// approval, and none of them changed anything.
|
|
167
|
-
return resumeFailed(`That approval could not be applied (HTTP 404). The run may have expired — ` +
|
|
168
|
-
`BrowserStack AI stops waiting after five minutes — or it may already have ` +
|
|
169
|
-
`been answered. Nothing was changed by this call. Start the task again if it ` +
|
|
170
|
-
`still needs doing.`);
|
|
171
|
-
}
|
|
172
|
-
// PARKED AGAIN: the task needs more than one approval. Handed straight back rather
|
|
173
|
-
// than answered here, because the whole point is that a human decides each one.
|
|
174
|
-
const parked = parkedResult(response.body, product);
|
|
175
|
-
if (parked) {
|
|
176
|
-
logger.info("askBrowserStackAI: run parked again after a decision (run=%s)", runId);
|
|
177
|
-
return needsApprovalResult(parked.runId, parked.ask, product);
|
|
178
|
-
}
|
|
179
|
-
// Finished. Through the SAME builder the start path uses, so an entitlement refusal,
|
|
180
|
-
// a 401 and a plain failure read identically whichever call produced them.
|
|
181
|
-
return buildResult(response, [], mode, product);
|
|
182
|
-
}
|
|
183
|
-
/** A resume that did not happen. Every field says "nothing changed", because in each
|
|
184
|
-
* case nothing did — and a caller that cannot tell a failed resume from a refusal
|
|
185
|
-
* either retries a write that landed or abandons one that never did. */
|
|
186
|
-
export function resumeFailed(error) {
|
|
187
|
-
return {
|
|
188
|
-
ok: false,
|
|
189
|
-
status: "error",
|
|
190
|
-
answer: null,
|
|
191
|
-
approvals: [],
|
|
192
|
-
approvals_source: "mcp",
|
|
193
|
-
elicitations: [],
|
|
194
|
-
needs_approval: [],
|
|
195
|
-
applied_before_stop: false,
|
|
196
|
-
permission_relay: { used: true, reason: "", detail: "" },
|
|
197
|
-
atlas_response: null,
|
|
198
|
-
error,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
/** A decision that went out and whose reply never came back.
|
|
202
|
-
*
|
|
203
|
-
* Deliberately NOT `resumeFailed`: every field there says "nothing changed", and that
|
|
204
|
-
* is a claim this case cannot make. The decision request carries the continuation now,
|
|
205
|
-
* so a lost reply covers everything from "it never arrived" to "the write landed and
|
|
206
|
-
* the answer was lost" — and telling a model nothing happened is what gets the change
|
|
207
|
-
* made twice.
|
|
208
|
-
*
|
|
209
|
-
* `applied_before_stop: null` for the same reason: null is "not measured", which is
|
|
210
|
-
* true, where false would be a measurement nobody took. */
|
|
211
|
-
export function decisionUnknown() {
|
|
212
|
-
return {
|
|
213
|
-
ok: false,
|
|
214
|
-
status: "error",
|
|
215
|
-
answer: null,
|
|
216
|
-
approvals: [],
|
|
217
|
-
approvals_source: "mcp",
|
|
218
|
-
elicitations: [],
|
|
219
|
-
needs_approval: [],
|
|
220
|
-
applied_before_stop: null,
|
|
221
|
-
permission_relay: {
|
|
222
|
-
used: true,
|
|
223
|
-
reason: "outcome_unknown",
|
|
224
|
-
detail: RELAY_OFF_DETAILS.outcome_unknown,
|
|
225
|
-
},
|
|
226
|
-
atlas_response: null,
|
|
227
|
-
error: "The approval was sent to BrowserStack AI but no reply came back, so it is not " +
|
|
228
|
-
"known whether the step completed. Nothing should be retried on this run.",
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
/** Starting a run with no task. Only reachable now that `query` is optional, which it
|
|
232
|
-
* has to be so a model can follow `next_step` — which omits it — literally. */
|
|
233
|
-
export function missingQuery() {
|
|
234
|
-
return resumeFailed("`query` is required to start a run: say what you want in plain language. " +
|
|
235
|
-
"(It is only omitted when answering a pending approval, which also needs " +
|
|
236
|
-
"`run_id`, `perm_id` and `decision`.) Nothing was sent, so nothing changed.");
|
|
237
|
-
}
|
|
238
|
-
export function incompleteResume() {
|
|
239
|
-
return resumeFailed("Resuming needs all three of `run_id`, `perm_id` and `decision`. Nothing was " +
|
|
240
|
-
"sent, so nothing changed and the pending approval is still waiting — ask the " +
|
|
241
|
-
"user whether to allow it and call again with all three.");
|
|
242
|
-
}
|