@browserstack/mcp-server 2.3.0-beta.5 → 2.3.0-beta.7
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/devicecontrol.capability-index.json +153 -18
- package/capability/mobiledevicecontrol.capability-index.json +540 -76
- package/dist/tools/applive.js +10 -1
- package/dist/tools/capability-registry/fallback.d.ts +40 -0
- package/dist/tools/capability-registry/fallback.js +61 -0
- package/dist/tools/capability-registry/index-loader.js +46 -4
- package/dist/tools/capability-registry/register.js +21 -11
- package/dist/tools/capability-registry/search.js +2 -1
- package/dist/tools/capability-registry/types.d.ts +64 -20
- package/dist/tools/capability-registry/types.js +13 -0
- package/dist/tools/failurelogs-utils/app-automate.js +1 -1
- package/dist/tools/live.js +8 -1
- package/package.json +1 -1
package/dist/tools/applive.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "fs";
|
|
|
3
3
|
import { startSession } from "./applive-utils/start-session.js";
|
|
4
4
|
import logger from "../logger.js";
|
|
5
5
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
|
+
import { isEnabled as capabilityRegistryEnabled } from "./capability-registry/config.js";
|
|
6
7
|
/**
|
|
7
8
|
* Launches an App Live Session on BrowserStack.
|
|
8
9
|
*/
|
|
@@ -54,7 +55,15 @@ export async function startAppLiveSession(args, config) {
|
|
|
54
55
|
}
|
|
55
56
|
export default function addAppLiveTools(server, config) {
|
|
56
57
|
const tools = {};
|
|
57
|
-
|
|
58
|
+
// A Live session is for a PERSON. The agent cannot tap, type, read the screen or take a
|
|
59
|
+
// screenshot in it, and every eval where an agent was asked to operate an app and chose
|
|
60
|
+
// this tool ended with "my tools can start it but can't tap anything in it". The
|
|
61
|
+
// description says so, and names the surface that can — only when that surface is on.
|
|
62
|
+
const description = "Open a manual App Live session for the user to operate by hand. It cannot be driven by the agent." +
|
|
63
|
+
(capabilityRegistryEnabled()
|
|
64
|
+
? " To tap, type or read the app yourself, use a mobiledevicecontrol session via invokeCapability."
|
|
65
|
+
: "");
|
|
66
|
+
tools.runAppLiveSession = server.tool("runAppLiveSession", description, {
|
|
58
67
|
desiredPhone: z
|
|
59
68
|
.string()
|
|
60
69
|
.describe("The full name of the device to run the app on. Example: 'iPhone 12 Pro' or 'Samsung Galaxy S20' or 'Google Pixel 6'. Always ask the user for the device they want to use, do not assume it. "),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Try the next thing when the product refuses, as the index says to.
|
|
3
|
+
*
|
|
4
|
+
* ONE REQUEST, ONE RESPONSE is still the rule for every capability that declares nothing
|
|
5
|
+
* here. A `fallbacks` list is the one authored exception: when THIS call answers a given
|
|
6
|
+
* status, the index knows a sibling capability that may do the job, and names it with the
|
|
7
|
+
* body to send. The chain exists for hide_keyboard on an iPhone, where the plain call
|
|
8
|
+
* answers 400 because the keyboard has no dismiss key, and the only way on is to press one
|
|
9
|
+
* of its own keys — Done where it exists, Return where it does not.
|
|
10
|
+
*
|
|
11
|
+
* The caller's consent covers the chain: it was given for the capability invoked, and the
|
|
12
|
+
* chain is that capability's published contract. What a step may do — press Return, which
|
|
13
|
+
* an app may treat as submit — is therefore the capability's guidance to state, not
|
|
14
|
+
* something this module decides. It does refuse two things no guidance could make right: a
|
|
15
|
+
* step naming a destructive capability, and one naming a capability this surface will not
|
|
16
|
+
* call.
|
|
17
|
+
*
|
|
18
|
+
* Every attempt is reported, in order, with the status it got, so the caller knows which
|
|
19
|
+
* step did the work and can read the screen back afterwards.
|
|
20
|
+
*/
|
|
21
|
+
import { GroupedArguments } from "./bind.js";
|
|
22
|
+
import { InvokeResult } from "./resolve.js";
|
|
23
|
+
import { Capability } from "./types.js";
|
|
24
|
+
export interface Attempt {
|
|
25
|
+
capability: string;
|
|
26
|
+
status: number;
|
|
27
|
+
/** The authored body the step sent, so the caller can see which keys were tried. */
|
|
28
|
+
body?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export interface FallbackOutcome {
|
|
31
|
+
/** The last response: the first 2xx, or the final refusal. */
|
|
32
|
+
result: InvokeResult;
|
|
33
|
+
/** Present only when the chain ran; the first entry is the original call. */
|
|
34
|
+
attempts?: Attempt[];
|
|
35
|
+
}
|
|
36
|
+
/** Makes one request for one capability — `invoke`, with the deps already bound. */
|
|
37
|
+
export type Runner = (capability: Capability, args: GroupedArguments) => Promise<InvokeResult>;
|
|
38
|
+
/** Resolves a step's name within the product of the capability being invoked. */
|
|
39
|
+
export type Lookup = (name: string) => Capability;
|
|
40
|
+
export declare function invokeWithFallbacks(capability: Capability, args: GroupedArguments, run: Runner, lookup: Lookup): Promise<FallbackOutcome>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Try the next thing when the product refuses, as the index says to.
|
|
3
|
+
*
|
|
4
|
+
* ONE REQUEST, ONE RESPONSE is still the rule for every capability that declares nothing
|
|
5
|
+
* here. A `fallbacks` list is the one authored exception: when THIS call answers a given
|
|
6
|
+
* status, the index knows a sibling capability that may do the job, and names it with the
|
|
7
|
+
* body to send. The chain exists for hide_keyboard on an iPhone, where the plain call
|
|
8
|
+
* answers 400 because the keyboard has no dismiss key, and the only way on is to press one
|
|
9
|
+
* of its own keys — Done where it exists, Return where it does not.
|
|
10
|
+
*
|
|
11
|
+
* The caller's consent covers the chain: it was given for the capability invoked, and the
|
|
12
|
+
* chain is that capability's published contract. What a step may do — press Return, which
|
|
13
|
+
* an app may treat as submit — is therefore the capability's guidance to state, not
|
|
14
|
+
* something this module decides. It does refuse two things no guidance could make right: a
|
|
15
|
+
* step naming a destructive capability, and one naming a capability this surface will not
|
|
16
|
+
* call.
|
|
17
|
+
*
|
|
18
|
+
* Every attempt is reported, in order, with the status it got, so the caller knows which
|
|
19
|
+
* step did the work and can read the screen back afterwards.
|
|
20
|
+
*/
|
|
21
|
+
import { InvocationError } from "./index-loader.js";
|
|
22
|
+
function handle(capability) {
|
|
23
|
+
return capability.name || `${capability.method} ${capability.path}`;
|
|
24
|
+
}
|
|
25
|
+
export async function invokeWithFallbacks(capability, args, run, lookup) {
|
|
26
|
+
let result = await run(capability, args);
|
|
27
|
+
const chain = capability.fallbacks ?? [];
|
|
28
|
+
if (result.ok || chain.length === 0)
|
|
29
|
+
return { result };
|
|
30
|
+
const attempts = [
|
|
31
|
+
{ capability: handle(capability), status: result.http_response.status },
|
|
32
|
+
];
|
|
33
|
+
for (const step of chain) {
|
|
34
|
+
// Matched against the LATEST status, not the first: a step that answers something
|
|
35
|
+
// the next step does not name ends the chain there, with that answer.
|
|
36
|
+
if (result.http_response.status !== step.when.status)
|
|
37
|
+
continue;
|
|
38
|
+
const next = lookup(step.capability);
|
|
39
|
+
if (next.mode === "destructive")
|
|
40
|
+
throw new InvocationError(`${handle(capability)} names ${step.capability} as a fallback, which is destructive and cannot run`);
|
|
41
|
+
if (next.invocable === false)
|
|
42
|
+
throw new InvocationError(`${handle(capability)} names ${step.capability} as a fallback, which cannot be called on this server`);
|
|
43
|
+
// Only the path parameters travel: the step's body and query are the index's own, so
|
|
44
|
+
// a caller cannot steer the chain into sending something the contract never showed.
|
|
45
|
+
result = await run(next, {
|
|
46
|
+
path_params: args.path_params,
|
|
47
|
+
query: step.query,
|
|
48
|
+
body: step.body,
|
|
49
|
+
});
|
|
50
|
+
attempts.push({
|
|
51
|
+
capability: step.capability,
|
|
52
|
+
status: result.http_response.status,
|
|
53
|
+
...(step.body ? { body: step.body } : {}),
|
|
54
|
+
});
|
|
55
|
+
if (result.ok)
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
// A chain that matched nothing is not a chain that ran: the caller sees the plain
|
|
59
|
+
// refusal, as it would from a capability that declares no fallbacks at all.
|
|
60
|
+
return attempts.length > 1 ? { result, attempts } : { result };
|
|
61
|
+
}
|
|
@@ -100,14 +100,53 @@ function asReference(capability) {
|
|
|
100
100
|
]) {
|
|
101
101
|
delete reference[field];
|
|
102
102
|
}
|
|
103
|
+
// Guidance is NOT touched here. `withSurfaceGuidance` has already folded in
|
|
104
|
+
// `remote_guidance` — which is where a reference-only capability says what to do
|
|
105
|
+
// instead — and it runs for every capability, not only this branch.
|
|
106
|
+
// `withSurfaceGuidance` has already run, so guidance is a plain array here — and for a
|
|
107
|
+
// reference-only capability its `remote` half is where "what to do instead" lives.
|
|
108
|
+
const named = Array.isArray(capability.guidance)
|
|
109
|
+
? capability.guidance.length > 0
|
|
110
|
+
: false;
|
|
103
111
|
return {
|
|
104
112
|
...reference,
|
|
105
113
|
invocable: false,
|
|
106
|
-
|
|
107
|
-
?
|
|
108
|
-
: ["This capability cannot be called on this server."],
|
|
114
|
+
...(named
|
|
115
|
+
? {}
|
|
116
|
+
: { guidance: ["This capability cannot be called on this server."] }),
|
|
109
117
|
};
|
|
110
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Flatten `guidance` to the lines that apply to the surface doing the loading.
|
|
121
|
+
*
|
|
122
|
+
* A capability may say three things about how to use it: what is true everywhere, what is
|
|
123
|
+
* true only where the server sits beside the user, and what is true only on a remote one.
|
|
124
|
+
* Only one of the last two can ever apply, so the caller is handed exactly that and never
|
|
125
|
+
* has to work out which half was meant for them.
|
|
126
|
+
*
|
|
127
|
+
* COMPOSED, NOT REPLACED. An earlier shape put the remote instruction in a single
|
|
128
|
+
* `unavailable_note` that replaced the guidance outright, which threw away lines still true
|
|
129
|
+
* there — the accepted file types, how long a large upload takes — and invited them to be
|
|
130
|
+
* restated in the note, where the two copies could drift. Shared lines are now written once.
|
|
131
|
+
*
|
|
132
|
+
* The object form is flattened away here, so no reader downstream sees anything but a plain
|
|
133
|
+
* array, and which surface this is stays ours to know rather than a fact the caller acts on.
|
|
134
|
+
*/
|
|
135
|
+
function withSurfaceGuidance(capability) {
|
|
136
|
+
const g = capability.guidance;
|
|
137
|
+
if (!g || Array.isArray(g))
|
|
138
|
+
return capability;
|
|
139
|
+
const lines = [
|
|
140
|
+
...(g.shared ?? []),
|
|
141
|
+
...(appConfig.REMOTE_MCP ? (g.remote ?? []) : (g.local ?? [])),
|
|
142
|
+
];
|
|
143
|
+
const next = { ...capability };
|
|
144
|
+
if (lines.length)
|
|
145
|
+
next.guidance = lines;
|
|
146
|
+
else
|
|
147
|
+
delete next.guidance;
|
|
148
|
+
return next;
|
|
149
|
+
}
|
|
111
150
|
function isPlainObject(value) {
|
|
112
151
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
152
|
}
|
|
@@ -237,11 +276,14 @@ export class CapabilityRegistry {
|
|
|
237
276
|
const names = new Map();
|
|
238
277
|
const withheld = new Map();
|
|
239
278
|
const enabled = [];
|
|
240
|
-
for (
|
|
279
|
+
for (let capability of bundle.capabilities) {
|
|
241
280
|
// UNIQUENESS IS CHECKED OVER EVERY CAPABILITY, disabled ones included. A clash
|
|
242
281
|
// between a disabled name and a live one is still a broken artifact, and skipping
|
|
243
282
|
// the disabled half would mean re-enabling the flag could fail the load later,
|
|
244
283
|
// somewhere with less context than here.
|
|
284
|
+
// Surface guidance is folded in for EVERY capability, before any of the branches
|
|
285
|
+
// below, so a reference-only one and an ordinary one are composed the same way.
|
|
286
|
+
capability = withSurfaceGuidance(capability);
|
|
245
287
|
if (isReferenceOnly(capability)) {
|
|
246
288
|
// PUBLISHED, BUT NOT CALLABLE. It keeps its name and intent so the search that
|
|
247
289
|
// should find it does, and loses everything a caller would need to invoke it.
|
|
@@ -22,8 +22,10 @@ import { redact } from "./redact.js";
|
|
|
22
22
|
import { fetchTransport } from "./egress.js";
|
|
23
23
|
import { CapabilityRegistry, InvocationError, resolveResponses, } from "./index-loader.js";
|
|
24
24
|
import { narrowResponse } from "./narrow.js";
|
|
25
|
+
import { invokeWithFallbacks } from "./fallback.js";
|
|
25
26
|
import { invoke } from "./resolve.js";
|
|
26
27
|
import { ambiguousProducts, productAnchors, isBrowseQuery, searchCapabilities, singular, terms, vocabularyOf, } from "./search.js";
|
|
28
|
+
import { guidanceLines } from "./types.js";
|
|
27
29
|
export const PERMISSION_VALUES = ["not_asked", "granted", "denied"];
|
|
28
30
|
/**
|
|
29
31
|
* The search-side twin of `user_permission`: did a human choose the product, or did you?
|
|
@@ -702,8 +704,8 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
702
704
|
mode: capability.mode,
|
|
703
705
|
entity: capability.entity,
|
|
704
706
|
...(capability.intent ? { intent: capability.intent } : {}),
|
|
705
|
-
...(capability.guidance
|
|
706
|
-
? { guidance: capability.guidance }
|
|
707
|
+
...(guidanceLines(capability.guidance).length
|
|
708
|
+
? { guidance: guidanceLines(capability.guidance) }
|
|
707
709
|
: {}),
|
|
708
710
|
})),
|
|
709
711
|
...rest,
|
|
@@ -967,7 +969,7 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
967
969
|
// refusal carries the instruction, because the note is the whole point of it
|
|
968
970
|
// still being published.
|
|
969
971
|
return refuse("invokeCapability", "capability_not_invocable", `${handle} cannot be called on this server. ` +
|
|
970
|
-
(capability.guidance
|
|
972
|
+
(guidanceLines(capability.guidance)[0] ??
|
|
971
973
|
"Describe it for what to do instead.") +
|
|
972
974
|
" Follow that instead of retrying.", undefined, identity);
|
|
973
975
|
}
|
|
@@ -993,7 +995,9 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
993
995
|
return refuse("invokeCapability", "change_summary_missing", "change_summary is required: state what will change", undefined, identity);
|
|
994
996
|
}
|
|
995
997
|
}
|
|
996
|
-
|
|
998
|
+
// The base URL is resolved per target rather than once: a fallback step may sit
|
|
999
|
+
// on a different named host from the capability that declared it.
|
|
1000
|
+
const { result, attempts } = await invokeWithFallbacks(capability, args, async (target, targetArgs) => invoke(target, targetArgs, await deps.baseUrlFor(product, target.host), deps.credentialsFor(), transport, registry.index.products[product]?.auth, product), (name) => registry.byNameLookup(name, product).capability);
|
|
997
1001
|
// NARROWING IS APPLIED TO A SUCCESS ONLY. A 4xx body is the diagnosis the caller
|
|
998
1002
|
// needs whole, and a selection that happened to match none of its keys would
|
|
999
1003
|
// quietly empty it. `narrowResponse` refuses an unsupported `select` by throwing,
|
|
@@ -1001,13 +1005,18 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
1001
1005
|
const narrowed = result.ok
|
|
1002
1006
|
? narrowResponse(capability, result.http_response.body, input.select)
|
|
1003
1007
|
: undefined;
|
|
1004
|
-
const answer =
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1008
|
+
const answer = {
|
|
1009
|
+
...(narrowed
|
|
1010
|
+
? {
|
|
1011
|
+
...result,
|
|
1012
|
+
http_response: { ...result.http_response, body: narrowed.body },
|
|
1013
|
+
narrowed: narrowed.narrowing,
|
|
1014
|
+
}
|
|
1015
|
+
: result),
|
|
1016
|
+
// Only when the chain ran: a capability without `fallbacks` answers exactly as
|
|
1017
|
+
// before, and one whose first call succeeded has nothing to report.
|
|
1018
|
+
...(attempts ? { fallback: { attempts } } : {}),
|
|
1019
|
+
};
|
|
1011
1020
|
// The one row for a completed invoke. invoke() returns a 4xx/5xx rather than
|
|
1012
1021
|
// throwing, so `success` keeps its tool-level meaning and these two fields carry
|
|
1013
1022
|
// whether the product call worked. Status 0 means it could not be reached.
|
|
@@ -1016,6 +1025,7 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
1016
1025
|
upstream_ok: result.ok,
|
|
1017
1026
|
upstream_status: result.http_response.status,
|
|
1018
1027
|
change_summary: redact(input.change_summary),
|
|
1028
|
+
...(attempts ? { fallback_steps: attempts.length - 1 } : {}),
|
|
1019
1029
|
...(narrowed
|
|
1020
1030
|
? {
|
|
1021
1031
|
narrowed_from: narrowed.narrowing.from,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* * CARDINALITY. A "list" query answered by a single-record getter sends the caller to a
|
|
12
12
|
* capability needing an id it cannot possibly have yet.
|
|
13
13
|
*/
|
|
14
|
+
import { guidanceLines, } from "./types.js";
|
|
14
15
|
/**
|
|
15
16
|
* Every non-alphanumeric character separates, `_` included.
|
|
16
17
|
*
|
|
@@ -408,7 +409,7 @@ function score(capability, wanted, weights, aliases, hint, plural) {
|
|
|
408
409
|
// `test_cases_count`); gating it on an identity match instead made a field reachable
|
|
409
410
|
// only through returns unreachable, which is worse.
|
|
410
411
|
[(capability.returns || []).join(" "), 1],
|
|
411
|
-
[(capability.guidance
|
|
412
|
+
[guidanceLines(capability.guidance).join(" "), 1],
|
|
412
413
|
// Parameter names, their descriptions, and their enum values — 330 descriptions and 34
|
|
413
414
|
// value lists that the artifact already carries and nothing was reading. The vocabulary
|
|
414
415
|
// a caller uses is often the value they mean to send: `pass` and `fail` appear nowhere
|
|
@@ -102,6 +102,53 @@ export interface ResponseDoc extends ComponentRef {
|
|
|
102
102
|
schema?: SchemaNode;
|
|
103
103
|
}
|
|
104
104
|
/** A capability, keyed by the endpoint it exposes. There is deliberately no name. */
|
|
105
|
+
/**
|
|
106
|
+
* How to call the endpoint correctly, optionally split by which surface is serving it.
|
|
107
|
+
*
|
|
108
|
+
* A plain array is shared — true wherever this runs, which is almost everything, and the
|
|
109
|
+
* form every shipped index already uses. The object form adds lines that are true on only
|
|
110
|
+
* one surface: `local` where the server sits beside the user, `remote` where it does not.
|
|
111
|
+
* `shared` is the same list the plain array would have been, so a capability can carry
|
|
112
|
+
* facts of both kinds without repeating any of them.
|
|
113
|
+
*
|
|
114
|
+
* The loader folds the applicable half into one array before anything reads it, so a
|
|
115
|
+
* caller is handed exactly the guidance that applies to the server it is talking to and
|
|
116
|
+
* never has to work out which half was meant for it. The split itself is ours to know:
|
|
117
|
+
* these keys do not survive into what describeCapability or searchCapability return.
|
|
118
|
+
*
|
|
119
|
+
* WHY A LINE IS SURFACE-SPECIFIC RATHER THAN SHARED: on a remote server, advice about the
|
|
120
|
+
* caller's own filesystem is not merely useless but wrong — it would have an agent tell the
|
|
121
|
+
* user to configure a machine they are not on. And for a `local_only` capability, `remote`
|
|
122
|
+
* is where "what to do instead" lives, which searchCapability puts in front of the caller
|
|
123
|
+
* before they pick anything, let alone call it.
|
|
124
|
+
*/
|
|
125
|
+
export type Guidance = string[] | {
|
|
126
|
+
shared?: string[];
|
|
127
|
+
local?: string[];
|
|
128
|
+
remote?: string[];
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* The guidance lines as a plain array, whichever form was authored.
|
|
132
|
+
*
|
|
133
|
+
* The loader flattens the object form at load, so everything downstream genuinely does hold
|
|
134
|
+
* an array — this exists so that is checked rather than asserted, and so a capability read
|
|
135
|
+
* straight from a file (a test, a checker) behaves the same as one from the registry.
|
|
136
|
+
* Without the surface to decide between them, only the shared lines can be returned.
|
|
137
|
+
*/
|
|
138
|
+
export declare function guidanceLines(guidance?: Guidance): string[];
|
|
139
|
+
/** One step of a capability's `fallbacks` chain. */
|
|
140
|
+
export interface Fallback {
|
|
141
|
+
/** Runs when the latest response carried this status. */
|
|
142
|
+
when: {
|
|
143
|
+
status: number;
|
|
144
|
+
};
|
|
145
|
+
/** The sibling capability to call, by published name, in the same product. */
|
|
146
|
+
capability: string;
|
|
147
|
+
/** The body to send it — authored, never the caller's. */
|
|
148
|
+
body?: Record<string, unknown>;
|
|
149
|
+
/** Query parameters to send it, if it takes any. */
|
|
150
|
+
query?: Record<string, unknown>;
|
|
151
|
+
}
|
|
105
152
|
export interface Capability {
|
|
106
153
|
/**
|
|
107
154
|
* The caller-facing handle — the snake-cased `operationId`, unique within its product.
|
|
@@ -139,14 +186,8 @@ export interface Capability {
|
|
|
139
186
|
query?: WireParam[];
|
|
140
187
|
body?: WireParam[];
|
|
141
188
|
intent?: string;
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
* ABSENT from the released artifact — the export dropped it, and the shape-change note
|
|
146
|
-
* does not say whether that was intended. Kept optional and still scored by search so the
|
|
147
|
-
* server works either way; nothing may depend on it being present.
|
|
148
|
-
*/
|
|
149
|
-
guidance?: string[];
|
|
189
|
+
/** How to call it; see `Guidance` for the local/remote split. */
|
|
190
|
+
guidance?: Guidance;
|
|
150
191
|
/** Allowlisted row fields. Absent when `shape` is "discovered". */
|
|
151
192
|
returns?: string[];
|
|
152
193
|
/** "discovered" when the product declares no response schema for this operation. */
|
|
@@ -189,18 +230,6 @@ export interface Capability {
|
|
|
189
230
|
* than hunting for a synonym.
|
|
190
231
|
*/
|
|
191
232
|
local_only?: boolean;
|
|
192
|
-
/**
|
|
193
|
-
* What to do instead, on a surface where this capability cannot run.
|
|
194
|
-
*
|
|
195
|
-
* A `local_only` capability is NOT hidden on a remote server: the query that should find
|
|
196
|
-
* it still finds it, because an agent asked to upload a file from the user's machine
|
|
197
|
-
* needs to learn that the route exists and what replaces it here. What it loses is the
|
|
198
|
-
* means to call it — the parameters and response shapes are stripped at load, and
|
|
199
|
-
* invocation is refused — so the note is the whole of what remains.
|
|
200
|
-
*
|
|
201
|
-
* Write it as the instruction the caller should follow, not as an apology.
|
|
202
|
-
*/
|
|
203
|
-
unavailable_note?: string;
|
|
204
233
|
/**
|
|
205
234
|
* FALSE when this build publishes the capability for reference but will not call it.
|
|
206
235
|
*
|
|
@@ -255,6 +284,21 @@ export interface Capability {
|
|
|
255
284
|
/** One line for the contract: why this is here rather than on the wire. */
|
|
256
285
|
note?: string;
|
|
257
286
|
};
|
|
287
|
+
/**
|
|
288
|
+
* What to try next when the product refuses THIS call, in order.
|
|
289
|
+
*
|
|
290
|
+
* The one authored exception to one-request-one-response. Each step names a sibling
|
|
291
|
+
* capability in the same product and the fixed body to send it; the original call's
|
|
292
|
+
* path parameters are carried over, nothing else. A step runs only when the LATEST
|
|
293
|
+
* response has the status it names, and the chain stops at the first 2xx.
|
|
294
|
+
*
|
|
295
|
+
* Exists for hide_keyboard on an iPhone: the keyboard has no dismiss key, so the plain
|
|
296
|
+
* call is refused, and the only way on is to press one of the keys it does carry. The
|
|
297
|
+
* caller's consent covers the chain, because the chain is this capability's published
|
|
298
|
+
* contract — so what a step may do (press Return, which an app may treat as submit) is
|
|
299
|
+
* the capability's guidance to state. Every attempt is reported with its status.
|
|
300
|
+
*/
|
|
301
|
+
fallbacks?: Fallback[];
|
|
258
302
|
paginated?: boolean;
|
|
259
303
|
/** The largest page the operation declares. */
|
|
260
304
|
max_page_size?: number;
|
|
@@ -31,3 +31,16 @@ export const ENVELOPE_KEYS = [
|
|
|
31
31
|
"harness_commit",
|
|
32
32
|
"products",
|
|
33
33
|
];
|
|
34
|
+
/**
|
|
35
|
+
* The guidance lines as a plain array, whichever form was authored.
|
|
36
|
+
*
|
|
37
|
+
* The loader flattens the object form at load, so everything downstream genuinely does hold
|
|
38
|
+
* an array — this exists so that is checked rather than asserted, and so a capability read
|
|
39
|
+
* straight from a file (a test, a checker) behaves the same as one from the registry.
|
|
40
|
+
* Without the surface to decide between them, only the shared lines can be returned.
|
|
41
|
+
*/
|
|
42
|
+
export function guidanceLines(guidance) {
|
|
43
|
+
if (!guidance)
|
|
44
|
+
return [];
|
|
45
|
+
return Array.isArray(guidance) ? guidance : (guidance.shared ?? []);
|
|
46
|
+
}
|
|
@@ -4,7 +4,7 @@ import { apiClient } from "../../lib/apiClient.js";
|
|
|
4
4
|
import { wrapUntrusted } from "../../lib/untrusted-content.js";
|
|
5
5
|
// DEVICE LOGS
|
|
6
6
|
export async function retrieveDeviceLogs(sessionId, buildId, config) {
|
|
7
|
-
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/
|
|
7
|
+
const url = `https://api.browserstack.com/app-automate/builds/${buildId}/sessions/${sessionId}/devicelogs`;
|
|
8
8
|
const authString = getBrowserStackAuth(config);
|
|
9
9
|
const auth = Buffer.from(authString).toString("base64");
|
|
10
10
|
const response = await apiClient.get({
|
package/dist/tools/live.js
CHANGED
|
@@ -3,6 +3,7 @@ import logger from "../logger.js";
|
|
|
3
3
|
import { startBrowserSession } from "./live-utils/start-session.js";
|
|
4
4
|
import { PlatformType } from "./live-utils/types.js";
|
|
5
5
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
|
+
import { isEnabled as capabilityRegistryEnabled } from "./capability-registry/config.js";
|
|
6
7
|
import globalConfig from "../config.js";
|
|
7
8
|
// Define the schema shape
|
|
8
9
|
const LiveArgsShape = {
|
|
@@ -88,7 +89,13 @@ async function runBrowserSession(rawArgs, config) {
|
|
|
88
89
|
}
|
|
89
90
|
export default function addBrowserLiveTools(server, config) {
|
|
90
91
|
const tools = {};
|
|
91
|
-
|
|
92
|
+
// Same rule as runAppLiveSession: Live is for a person, and an agent that picks it for a
|
|
93
|
+
// click-by-locator task ends up reporting that Live cannot click by locator.
|
|
94
|
+
const description = "Open a manual Live browser session (desktop or mobile) for the user to operate by hand. It cannot be driven by the agent." +
|
|
95
|
+
(capabilityRegistryEnabled()
|
|
96
|
+
? " To click, type or read the page yourself, use a devicecontrol session via invokeCapability."
|
|
97
|
+
: "");
|
|
98
|
+
tools.runBrowserLiveSession = server.tool("runBrowserLiveSession", description, LiveArgsShape, {
|
|
92
99
|
title: "Run Browser Live Session",
|
|
93
100
|
readOnlyHint: false,
|
|
94
101
|
openWorldHint: true,
|