@browserstack/mcp-server 1.5.1-beta.2 → 2.0.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/README.md +54 -1
- package/capability/tm.capability-index.json +50420 -0
- package/capability/tra.capability-index.json +52096 -0
- package/dist/lib/central-oauth.d.ts +133 -0
- package/dist/lib/central-oauth.js +246 -0
- package/dist/lib/instrumentation.d.ts +14 -1
- package/dist/lib/instrumentation.js +8 -1
- package/dist/server-factory.js +5 -0
- package/dist/tools/ask-browserstack/central-oauth.d.ts +25 -46
- package/dist/tools/ask-browserstack/central-oauth.js +55 -177
- package/dist/tools/capability-registry/bind.d.ts +28 -0
- package/dist/tools/capability-registry/bind.js +407 -0
- package/dist/tools/capability-registry/config.d.ts +62 -0
- package/dist/tools/capability-registry/config.js +218 -0
- package/dist/tools/capability-registry/discovery.d.ts +44 -0
- package/dist/tools/capability-registry/discovery.js +101 -0
- package/dist/tools/capability-registry/egress.d.ts +51 -0
- package/dist/tools/capability-registry/egress.js +197 -0
- package/dist/tools/capability-registry/index-loader.d.ts +162 -0
- package/dist/tools/capability-registry/index-loader.js +523 -0
- package/dist/tools/capability-registry/oauth.d.ts +63 -0
- package/dist/tools/capability-registry/oauth.js +176 -0
- package/dist/tools/capability-registry/redact.d.ts +34 -0
- package/dist/tools/capability-registry/redact.js +98 -0
- package/dist/tools/capability-registry/register.d.ts +48 -0
- package/dist/tools/capability-registry/register.js +956 -0
- package/dist/tools/capability-registry/resolve.d.ts +38 -0
- package/dist/tools/capability-registry/resolve.js +104 -0
- 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 +197 -0
- package/dist/tools/capability-registry/search.js +907 -0
- package/dist/tools/capability-registry/types.d.ts +341 -0
- package/dist/tools/capability-registry/types.js +33 -0
- 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 +10 -3
|
@@ -0,0 +1,341 @@
|
|
|
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
|
+
/**
|
|
139
|
+
* Withheld from this surface entirely.
|
|
140
|
+
*
|
|
141
|
+
* A DISABLED CAPABILITY DOES NOT EXIST for searchCapability, describeCapability,
|
|
142
|
+
* describeEntity or listProducts — it is filtered out when the registry loads, so no
|
|
143
|
+
* reader downstream has to remember to check. `invokeCapability` is the one exception:
|
|
144
|
+
* an exact-name or exact-endpoint invoke answers `capability_disabled` rather than
|
|
145
|
+
* `unknown_capability`, so a caller that already held the handle learns the absence is
|
|
146
|
+
* deliberate instead of hunting for a synonym.
|
|
147
|
+
*
|
|
148
|
+
* NO REASON IS CARRIED. It was a `boolean | string` union whose string half was the
|
|
149
|
+
* explanation, and it paid for itself nowhere: one consumer, on a path the flag itself
|
|
150
|
+
* makes rare, at 1216 bytes of duplicated prose in an artifact that ships to public npm.
|
|
151
|
+
* Why a capability is withheld is documentation — it belongs in the notes beside the
|
|
152
|
+
* decision, not in four copies inside the index.
|
|
153
|
+
*
|
|
154
|
+
* FAILS CLOSED. Any truthy value disables, not just `=== true`. A flag whose only job is
|
|
155
|
+
* suppression must not be ignored for arriving in an unexpected shape.
|
|
156
|
+
*/
|
|
157
|
+
disabled?: boolean;
|
|
158
|
+
paginated?: boolean;
|
|
159
|
+
/** The largest page the operation declares. */
|
|
160
|
+
max_page_size?: number;
|
|
161
|
+
/**
|
|
162
|
+
* Declared responses by status code, values possibly `{$response: "Name"}` references.
|
|
163
|
+
*
|
|
164
|
+
* ADDITIVE and not yet emitted: no capability in the current export carries it. Absence
|
|
165
|
+
* means "no response schema available", never an error. Resolve with `resolveComponent`.
|
|
166
|
+
*/
|
|
167
|
+
responses?: Record<string, ResponseDoc>;
|
|
168
|
+
}
|
|
169
|
+
export interface EntityDoc {
|
|
170
|
+
title?: string;
|
|
171
|
+
/**
|
|
172
|
+
* One line saying what this entity IS. Rides on `listProducts`, next to the aliases.
|
|
173
|
+
*
|
|
174
|
+
* Aliases route; they do not define. `version -> history, revision, version-history`
|
|
175
|
+
* tells a caller which words land here and nothing about whether that is the versioning
|
|
176
|
+
* of a test case or of a project. Without this line the only way to find out is
|
|
177
|
+
* `describeEntity`, once per entity — 19 calls at ~1.4KB for tm, against ~1.6KB to
|
|
178
|
+
* answer it for all of them at once.
|
|
179
|
+
*
|
|
180
|
+
* Capped at 140 characters by the build, and dropped rather than truncated when longer.
|
|
181
|
+
* Often absent: a product that has not authored these emits none (loadtesting has 0 of
|
|
182
|
+
* 9 today), and absence means "unwritten", never an error.
|
|
183
|
+
*/
|
|
184
|
+
description?: string;
|
|
185
|
+
aliases?: string[];
|
|
186
|
+
id_convention?: string;
|
|
187
|
+
parents?: string[];
|
|
188
|
+
relations?: {
|
|
189
|
+
entity?: string;
|
|
190
|
+
via?: string;
|
|
191
|
+
}[];
|
|
192
|
+
/**
|
|
193
|
+
* What a caller gets wrong about this ENTITY, as opposed to one operation.
|
|
194
|
+
*
|
|
195
|
+
* The entity-wide half of guidance: "a run's `id` is the display string, take `uuid`"
|
|
196
|
+
* holds for every capability returning a run, so it is stated once here rather than
|
|
197
|
+
* copied onto each capability's `guidance` — which would repeat across ~20 records and,
|
|
198
|
+
* since guidance is a search haystack, dilute ranking with boilerplate.
|
|
199
|
+
*
|
|
200
|
+
* Filtered at build time and often absent: of tm's 155 authored facts, 73 are publishable
|
|
201
|
+
* as written and the rest name routes, HTTP verbs or wire-shaped parameters. Absent means
|
|
202
|
+
* "none passed", never an error.
|
|
203
|
+
*/
|
|
204
|
+
key_facts?: string[];
|
|
205
|
+
[key: string]: unknown;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Paging controls, keyed "METHOD /path".
|
|
209
|
+
*
|
|
210
|
+
* NOT USED BY THIS SERVER: it performs one request and returns the response, so paging
|
|
211
|
+
* belongs to the caller and the page parameters are published with the endpoint's other
|
|
212
|
+
* query parameters. The field is still emitted by the build, so it stays described here
|
|
213
|
+
* rather than silently ignored — a consumer that DOES page can use it.
|
|
214
|
+
*/
|
|
215
|
+
export interface PagingRule {
|
|
216
|
+
page?: string;
|
|
217
|
+
size?: string;
|
|
218
|
+
/** The largest page the operation declares. Absent when the spec states no maximum. */
|
|
219
|
+
max?: number;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* How a product wants the caller's credentials presented.
|
|
223
|
+
*
|
|
224
|
+
* The OpenAPI `securityScheme` vocabulary, verbatim — `type` / `in` / `name` / `scheme` —
|
|
225
|
+
* because the spec already describes this and inventing a parallel taxonomy would mean
|
|
226
|
+
* translating between two of them forever. ONE scheme per product, not OpenAPI's list of
|
|
227
|
+
* alternatives: this server holds a username and an access key and nothing else, so
|
|
228
|
+
* "the first alternative we can satisfy" only ever had one answer.
|
|
229
|
+
*
|
|
230
|
+
* `template` is what makes it a contract rather than a guess — a product taking
|
|
231
|
+
* `{username}_{access_key}` is expressible instead of being a special case in the server.
|
|
232
|
+
* It names placeholders, never values: the credential itself stays in config, and the
|
|
233
|
+
* harness's `{ env: … }` value sources must NOT cross the boundary into the artifact.
|
|
234
|
+
*/
|
|
235
|
+
export interface AuthScheme {
|
|
236
|
+
/**
|
|
237
|
+
* "apiKey" puts the rendered template in a header; "http" with scheme "basic" encodes it;
|
|
238
|
+
* "oauth2" exchanges the caller's credentials for a JWT and sends `Authorization: Bearer`.
|
|
239
|
+
*
|
|
240
|
+
* Each value fixes BOTH where the value comes from and how it is presented. Only `apiKey`
|
|
241
|
+
* carries `in`/`name`, because it is the only one with no convention for where it goes —
|
|
242
|
+
* RFC 6750 settles that for a bearer token, which is why OpenAPI's own `oauth2` scheme has
|
|
243
|
+
* no location fields either. `in`, `name` and `template` are REFUSED on `oauth2` rather
|
|
244
|
+
* than ignored, so an author who writes one gets an error instead of a silent 401.
|
|
245
|
+
*/
|
|
246
|
+
type: "apiKey" | "http" | "oauth2";
|
|
247
|
+
/** apiKey only. Header is the sole supported location — see `egress.authHeaders`. */
|
|
248
|
+
in?: "header" | "cookie" | "query";
|
|
249
|
+
/** apiKey only: the header name, e.g. "Api-Token". */
|
|
250
|
+
name?: string;
|
|
251
|
+
/** http only. */
|
|
252
|
+
scheme?: string;
|
|
253
|
+
/** apiKey/http only. Defaults to `{username}:{access_key}`. */
|
|
254
|
+
template?: string;
|
|
255
|
+
/**
|
|
256
|
+
* oauth2 only: override the endpoint the JWT is minted at.
|
|
257
|
+
*
|
|
258
|
+
* Optional, and most products want the default. Bounded by an allowlist in `oauth.ts` —
|
|
259
|
+
* this field decides where the caller's credentials are POSTed, and an index is a data
|
|
260
|
+
* file that ships to public npm.
|
|
261
|
+
*/
|
|
262
|
+
token_url?: string;
|
|
263
|
+
/**
|
|
264
|
+
* oauth2 only: override the scope set. Order is not significant; it is sorted and deduped
|
|
265
|
+
* before it reaches the grant or the token cache.
|
|
266
|
+
*
|
|
267
|
+
* There is deliberately no `flow` beside this. The server implements one grant, so a field
|
|
268
|
+
* with one legal value could distinguish nothing — and an unvalidated one would read as
|
|
269
|
+
* though it selected behaviour while selecting nothing.
|
|
270
|
+
*/
|
|
271
|
+
scopes?: string[];
|
|
272
|
+
}
|
|
273
|
+
export interface ProductIndex {
|
|
274
|
+
summary: string;
|
|
275
|
+
/**
|
|
276
|
+
* Words that route to this product but are not the name of anything in it.
|
|
277
|
+
*
|
|
278
|
+
* The entity names and their aliases are what a product CONTAINS; this is what its users
|
|
279
|
+
* CALL it when they ask for something. `flaky` is the clearest case: both indexes talk
|
|
280
|
+
* about flakiness and neither models it as an entity, so the word was invisible to
|
|
281
|
+
* routing and a question about it read as naming no product at all. The other half is
|
|
282
|
+
* the reverse — `workspace`, `team`, `setting` and `suite` belong to both products and
|
|
283
|
+
* happen to be modelled by one, which let an incidental word settle a question it had no
|
|
284
|
+
* business settling. Listing a word here is a claim about vocabulary only; it never adds
|
|
285
|
+
* a capability and never widens a search.
|
|
286
|
+
*/
|
|
287
|
+
routing_terms?: string[];
|
|
288
|
+
/**
|
|
289
|
+
* How to authenticate to this product. Absent means the historical default: the caller's
|
|
290
|
+
* credentials as `Api-Token: {username}:{access_key}`, which is what every shipped index
|
|
291
|
+
* relies on today.
|
|
292
|
+
*/
|
|
293
|
+
auth?: AuthScheme;
|
|
294
|
+
/**
|
|
295
|
+
* The single host this product is served from, when it has one.
|
|
296
|
+
*
|
|
297
|
+
* A default, not the last word: config overrides it, and for a region-sharded product
|
|
298
|
+
* (one declaring `base_urls`) account discovery outranks it and this is the fallback.
|
|
299
|
+
*/
|
|
300
|
+
base_url?: string;
|
|
301
|
+
/**
|
|
302
|
+
* Candidate regional hosts, in probe order, for a product whose host depends on the
|
|
303
|
+
* ACCOUNT rather than the deployment.
|
|
304
|
+
*
|
|
305
|
+
* Declaring these makes the product region-sharded: the server asks each in turn with the
|
|
306
|
+
* caller's credentials and keeps the one that answers. A single fixed `base_url` cannot
|
|
307
|
+
* express this — it would send every account outside the default region to the wrong host,
|
|
308
|
+
* a failure invisible to anyone testing from inside that region.
|
|
309
|
+
*/
|
|
310
|
+
base_urls?: string[];
|
|
311
|
+
/**
|
|
312
|
+
* The endpoint to probe the candidates with. Derived from the capabilities when absent
|
|
313
|
+
* (see `discovery.probePath`); declare it when the derived choice would be wrong.
|
|
314
|
+
*/
|
|
315
|
+
probe_path?: string;
|
|
316
|
+
capabilities: Capability[];
|
|
317
|
+
entities: Record<string, EntityDoc>;
|
|
318
|
+
paging?: Record<string, PagingRule>;
|
|
319
|
+
/** Named responses, referenced as `{$response: "Name"}`. Additive; not yet emitted. */
|
|
320
|
+
responses?: Record<string, ResponseDoc>;
|
|
321
|
+
/** Named schemas, referenced as `{$schema: "Name"}`. Additive; not yet emitted. */
|
|
322
|
+
schemas?: Record<string, SchemaNode>;
|
|
323
|
+
}
|
|
324
|
+
/** Where one product's data came from. Logging and cache-busting only. */
|
|
325
|
+
export interface Provenance {
|
|
326
|
+
/** `<commit>_<UTC timestamp>` in the released shape. */
|
|
327
|
+
build_id: string;
|
|
328
|
+
/** Dotted content-generation counter, e.g. "1.2". Absent in the pre-release shape. */
|
|
329
|
+
version?: string;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The merged, in-memory model: every loaded file's product under its own name.
|
|
333
|
+
*
|
|
334
|
+
* This is the server's own structure, not the file format — one file carries one product,
|
|
335
|
+
* and the registry holds all of them so a caller can search across products.
|
|
336
|
+
*/
|
|
337
|
+
export interface RegistryIndex {
|
|
338
|
+
schema_version: number;
|
|
339
|
+
build_id: string;
|
|
340
|
+
products: Record<string, ProductIndex>;
|
|
341
|
+
}
|
|
@@ -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
|
+
];
|
|
@@ -19,7 +19,7 @@ import { getTestPlan, GetTestPlanSchema, } from "./testmanagement-utils/get-test
|
|
|
19
19
|
import { listSubTestPlans, ListSubTestPlansSchema, } from "./testmanagement-utils/list-sub-testplans.js";
|
|
20
20
|
import { getSubTestPlan, GetSubTestPlanSchema, } from "./testmanagement-utils/get-sub-testplan.js";
|
|
21
21
|
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
|
|
22
|
-
import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID,
|
|
22
|
+
import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID, PLAN_WRITES_VIA_CAPABILITIES, PROJECT_ID_ONLY_FOR_FOLDER, } from "./tool-handoff.js";
|
|
23
23
|
//TODO: Moving the traceMCP and catch block to the parent(server) function
|
|
24
24
|
/**
|
|
25
25
|
* Wrapper to call createProjectOrFolder util.
|
|
@@ -536,7 +536,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
536
536
|
}, (args, context) => createLCAStepsTool(args, context, config, server));
|
|
537
537
|
tools.listTestPlans = server.tool("listTestPlans", "List test plans in a BrowserStack Test Management project. Returns each plan's identifier (TP-*), name, status, description, dates, and active/closed test-run counts. Supports pagination." +
|
|
538
538
|
NEEDS_PROJECT_ID +
|
|
539
|
-
|
|
539
|
+
PLAN_WRITES_VIA_CAPABILITIES, ListTestPlansSchema.shape, {
|
|
540
540
|
title: "List Test Plans",
|
|
541
541
|
readOnlyHint: true,
|
|
542
542
|
openWorldHint: false,
|
|
@@ -546,7 +546,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
546
546
|
tools.getTestPlan = server.tool("getTestPlan", "Fetch a test plan by identifier (TP-*) from BrowserStack Test Management. Returns plan metadata, the full list of linked test runs, total test-case count across runs, and a status summary — suitable for generating test documentation or QA status reports." +
|
|
547
547
|
NEEDS_PROJECT_ID +
|
|
548
548
|
NEEDS_TEST_PLAN_ID +
|
|
549
|
-
|
|
549
|
+
PLAN_WRITES_VIA_CAPABILITIES, GetTestPlanSchema.shape, {
|
|
550
550
|
title: "Get Test Plan",
|
|
551
551
|
readOnlyHint: true,
|
|
552
552
|
openWorldHint: false,
|
|
@@ -556,7 +556,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
556
556
|
tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination." +
|
|
557
557
|
NEEDS_PROJECT_ID +
|
|
558
558
|
NEEDS_TEST_PLAN_ID +
|
|
559
|
-
|
|
559
|
+
PLAN_WRITES_VIA_CAPABILITIES, ListSubTestPlansSchema.shape, {
|
|
560
560
|
title: "List Sub Test Plans",
|
|
561
561
|
readOnlyHint: true,
|
|
562
562
|
openWorldHint: false,
|
|
@@ -566,7 +566,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
566
566
|
tools.getSubTestPlan = server.tool("getSubTestPlan", "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs." +
|
|
567
567
|
NEEDS_PROJECT_ID +
|
|
568
568
|
NEEDS_TEST_PLAN_ID +
|
|
569
|
-
|
|
569
|
+
PLAN_WRITES_VIA_CAPABILITIES, GetSubTestPlanSchema.shape, {
|
|
570
570
|
title: "Get Sub Test Plan",
|
|
571
571
|
readOnlyHint: true,
|
|
572
572
|
openWorldHint: false,
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* where the missing identifier comes from.
|
|
9
9
|
*
|
|
10
10
|
* Point at a sibling tool whenever one can produce the id — it is faster and more
|
|
11
|
-
* predictable than
|
|
11
|
+
* predictable than anything that has to reason its way there. When no tool here can, point
|
|
12
|
+
* at the CAPABILITY FUNCTIONS: they reach the product's own API surface, so they answer the
|
|
13
|
+
* id question directly. Name them as a group, never individually — which function to call
|
|
14
|
+
* is their own business, and a name written here goes stale the moment one is renamed.
|
|
12
15
|
*
|
|
13
16
|
* The one that matters most: 15 of the 17 Test Management tools require a project
|
|
14
17
|
* identifier and NONE of them accepts its absence, yet no tool in this server lists
|
|
@@ -35,21 +38,21 @@ export declare const NEEDS_TEST_PLAN_ID: string;
|
|
|
35
38
|
/**
|
|
36
39
|
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
37
40
|
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
38
|
-
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else.
|
|
39
|
-
* (
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. The capability functions can do them
|
|
42
|
+
* (Test Management's own API covers creating, updating, cloning and deleting a plan, and
|
|
43
|
+
* linking or unlinking its runs), so without this line the model reads the four read tools,
|
|
44
|
+
* finds no create, and reports the capability as absent — which is exactly what a QA eval
|
|
45
|
+
* concluded.
|
|
43
46
|
*
|
|
44
47
|
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
45
|
-
* inviting the model to route plan work
|
|
46
|
-
*
|
|
48
|
+
* inviting the model to route plan work elsewhere generally, because the tool descriptions
|
|
49
|
+
* otherwise say to prefer a specific tool whenever one fits.
|
|
47
50
|
*
|
|
48
|
-
* Caveat worth knowing:
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
+
* Caveat worth knowing: a capability write is pinned to human approval, so this path only
|
|
52
|
+
* completes on a client that can show a prompt. On one that cannot, the intended write is
|
|
53
|
+
* refused rather than performed.
|
|
51
54
|
*/
|
|
52
|
-
export declare const
|
|
55
|
+
export declare const PLAN_WRITES_VIA_CAPABILITIES: string;
|
|
53
56
|
/** A build id comes from either build-lookup tool. */
|
|
54
57
|
export declare const NEEDS_BUILD_ID: string;
|
|
55
58
|
/**
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* where the missing identifier comes from.
|
|
9
9
|
*
|
|
10
10
|
* Point at a sibling tool whenever one can produce the id — it is faster and more
|
|
11
|
-
* predictable than
|
|
11
|
+
* predictable than anything that has to reason its way there. When no tool here can, point
|
|
12
|
+
* at the CAPABILITY FUNCTIONS: they reach the product's own API surface, so they answer the
|
|
13
|
+
* id question directly. Name them as a group, never individually — which function to call
|
|
14
|
+
* is their own business, and a name written here goes stale the moment one is renamed.
|
|
12
15
|
*
|
|
13
16
|
* The one that matters most: 15 of the 17 Test Management tools require a project
|
|
14
17
|
* identifier and NONE of them accepts its absence, yet no tool in this server lists
|
|
@@ -21,8 +24,8 @@
|
|
|
21
24
|
*/
|
|
22
25
|
/** No tool lists projects, so this genuinely has to go to the agent. */
|
|
23
26
|
export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool here lists projects, so if you do not " +
|
|
24
|
-
|
|
25
|
-
"retry this tool with the identifier
|
|
27
|
+
"have one, use the capability functions to list the Test Management projects, then " +
|
|
28
|
+
"retry this tool with the identifier they return.";
|
|
26
29
|
/** A sibling tool can produce the id — prefer it over the agent. */
|
|
27
30
|
export function needsIdFrom(idLabel, sourceTool) {
|
|
28
31
|
return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
|
|
@@ -34,30 +37,30 @@ export function needsIdFrom(idLabel, sourceTool) {
|
|
|
34
37
|
* "create me a project" through the agent before letting the tool run.
|
|
35
38
|
*/
|
|
36
39
|
export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier. Creating a folder inside an EXISTING project " +
|
|
37
|
-
"needs that project's identifier (PR-*); no tool here lists projects, so
|
|
38
|
-
|
|
40
|
+
"needs that project's identifier (PR-*); no tool here lists projects, so use the " +
|
|
41
|
+
"capability functions to look it up.";
|
|
39
42
|
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
40
43
|
export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "listTestPlans");
|
|
41
44
|
/**
|
|
42
45
|
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
43
46
|
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
44
|
-
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else.
|
|
45
|
-
* (
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. The capability functions can do them
|
|
48
|
+
* (Test Management's own API covers creating, updating, cloning and deleting a plan, and
|
|
49
|
+
* linking or unlinking its runs), so without this line the model reads the four read tools,
|
|
50
|
+
* finds no create, and reports the capability as absent — which is exactly what a QA eval
|
|
51
|
+
* concluded.
|
|
49
52
|
*
|
|
50
53
|
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
51
|
-
* inviting the model to route plan work
|
|
52
|
-
*
|
|
54
|
+
* inviting the model to route plan work elsewhere generally, because the tool descriptions
|
|
55
|
+
* otherwise say to prefer a specific tool whenever one fits.
|
|
53
56
|
*
|
|
54
|
-
* Caveat worth knowing:
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
+
* Caveat worth knowing: a capability write is pinned to human approval, so this path only
|
|
58
|
+
* completes on a client that can show a prompt. On one that cannot, the intended write is
|
|
59
|
+
* refused rather than performed.
|
|
57
60
|
*/
|
|
58
|
-
export const
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
+
export const PLAN_WRITES_VIA_CAPABILITIES = " Creating a test plan or sub-plan, and linking or unlinking test runs on one, are not " +
|
|
62
|
+
"available as tools here: use the capability functions, which reach Test Management's " +
|
|
63
|
+
"own API for these operations. They ask you to confirm before changing anything.";
|
|
61
64
|
/** A build id comes from either build-lookup tool. */
|
|
62
65
|
export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
|
|
63
66
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserstack/mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
4
|
"description": "BrowserStack's Official MCP Server",
|
|
5
5
|
"mcpName": "io.github.browserstack/mcp-server",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,13 +15,20 @@
|
|
|
15
15
|
"dev": "tsx watch --clear-screen=false src/index.ts",
|
|
16
16
|
"test": "vitest run",
|
|
17
17
|
"lint": "eslint . --ext .ts",
|
|
18
|
-
"format": "prettier --write \"src/**/*.ts\""
|
|
18
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
19
|
+
"check:contract": "python3 scripts/check-contract.py capability/tm.capability-index.json --baseline scripts/contract-baseline/tm.json --quiet",
|
|
20
|
+
"eval:index": "tsx scripts/eval-index.mts",
|
|
21
|
+
"seed:live": "tsx scripts/seed-live.mts",
|
|
22
|
+
"seed:live:tra": "tsx scripts/seed-live-tra.mts",
|
|
23
|
+
"plan:tests": "tsx scripts/plan-capability-tests.mts",
|
|
24
|
+
"replay:live": "tsx scripts/replay-live.mts"
|
|
19
25
|
},
|
|
20
26
|
"bin": {
|
|
21
27
|
"browserstack-mcp-server": "dist/index.js"
|
|
22
28
|
},
|
|
23
29
|
"files": [
|
|
24
|
-
"dist"
|
|
30
|
+
"dist",
|
|
31
|
+
"capability"
|
|
25
32
|
],
|
|
26
33
|
"keywords": [
|
|
27
34
|
"mcp",
|