@browserstack/mcp-server 2.2.0-beta.1 → 2.2.0
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 +42 -432
- package/capability/loadtesting.capability-index.json +25 -2
- package/capability/tm.capability-index.json +1 -2
- package/dist/config.d.ts +27 -2
- package/dist/config.js +19 -6
- package/dist/lib/instrumentation.js +29 -3
- package/dist/lib/utils.js +8 -0
- package/dist/tools/accessiblity-utils/scanner.js +1 -1
- package/dist/tools/ask-browserstack/central-oauth.d.ts +46 -25
- package/dist/tools/ask-browserstack/central-oauth.js +177 -55
- package/dist/tools/ask-browserstack/config.d.ts +0 -14
- package/dist/tools/ask-browserstack/config.js +0 -16
- package/dist/tools/ask-browserstack/deferred.d.ts +74 -0
- package/dist/tools/ask-browserstack/deferred.js +242 -0
- package/dist/tools/ask-browserstack/register.d.ts +3 -2
- package/dist/tools/ask-browserstack/register.js +219 -72
- package/dist/tools/ask-browserstack/relay.js +11 -17
- package/dist/tools/ask-browserstack/stream.d.ts +27 -5
- package/dist/tools/ask-browserstack/stream.js +25 -14
- package/dist/tools/ask-browserstack/types.d.ts +54 -3
- package/dist/tools/ask-browserstack/types.js +11 -1
- package/dist/tools/automate-utils/fetch-screenshots.js +17 -15
- package/dist/tools/capability-registry/bind.js +7 -43
- package/dist/tools/capability-registry/discovery.js +1 -3
- package/dist/tools/capability-registry/egress.d.ts +5 -12
- package/dist/tools/capability-registry/egress.js +1 -24
- package/dist/tools/capability-registry/redact.js +7 -5
- package/dist/tools/capability-registry/register.js +41 -289
- package/dist/tools/capability-registry/resolve.d.ts +1 -1
- package/dist/tools/capability-registry/resolve.js +3 -24
- package/dist/tools/capability-registry/search.d.ts +1 -28
- package/dist/tools/capability-registry/search.js +4 -202
- package/dist/tools/capability-registry/types.d.ts +3 -42
- package/dist/tools/testmanagement.js +5 -5
- package/dist/tools/tool-handoff.d.ts +12 -15
- package/dist/tools/tool-handoff.js +18 -21
- package/package.json +3 -4
- package/capability/tra.capability-index.json +0 -52074
- package/dist/lib/central-oauth.d.ts +0 -133
- package/dist/lib/central-oauth.js +0 -246
- package/dist/tools/capability-registry/oauth.d.ts +0 -63
- package/dist/tools/capability-registry/oauth.js +0 -176
- package/dist/tools/capability-registry/routing-token.d.ts +0 -25
- package/dist/tools/capability-registry/routing-token.js +0 -87
|
@@ -493,25 +493,6 @@ function score(capability, wanted, weights, aliases, hint, plural) {
|
|
|
493
493
|
const WEAK_COVERAGE = 0.5;
|
|
494
494
|
/** The heaviest field, and so the yardstick a perfect match is measured against. */
|
|
495
495
|
const IDENTITY_WEIGHT = 6;
|
|
496
|
-
// THE CROSS-PRODUCT FLOOR WAS HERE, and is gone with the thing that needed it.
|
|
497
|
-
//
|
|
498
|
-
// It reserved two slots per matching product so a small product could not be shut out by
|
|
499
|
-
// a large one — tm has 173 capabilities to Load Testing's 20, and on a word both share
|
|
500
|
-
// ("test", "config", "tag") tm took the whole page. "add a tag to xyz test" returned
|
|
501
|
-
// tm tm LT tm tm, and an agent reading the first row went to the wrong product.
|
|
502
|
-
//
|
|
503
|
-
// `product` is now REQUIRED on searchCapability, so a search never spans products and the
|
|
504
|
-
// floor could never fire. The bug it fixed is fixed harder: the agent chooses the product
|
|
505
|
-
// deliberately against listProducts, or asks the user, instead of inferring it from which
|
|
506
|
-
// row happened to rank first. Keeping the code would mean carrying a branch nothing
|
|
507
|
-
// reaches and tests asserting behaviour the surface cannot produce.
|
|
508
|
-
//
|
|
509
|
-
// If searching across products ever returns — a "search everywhere" mode, or a caller
|
|
510
|
-
// that legitimately cannot know the product — this comes back with it. It is in the
|
|
511
|
-
// history at 617b4b9, and the reasoning above is why it was right.
|
|
512
|
-
/** Which products a query could plausibly be about, when more than one could. */
|
|
513
|
-
/** Words too common in this domain to settle a product on their own. */
|
|
514
|
-
const GENERIC_ANCHORS = new Set(["test", "tests", "testing"]);
|
|
515
496
|
/**
|
|
516
497
|
* Products whose OWN vocabulary the query hits, when no word in the query settles it.
|
|
517
498
|
*
|
|
@@ -558,97 +539,17 @@ export function ambiguousProducts(products, query) {
|
|
|
558
539
|
}
|
|
559
540
|
}
|
|
560
541
|
}
|
|
561
|
-
// WORDS A PRODUCT OWNS WITHOUT MODELLING THEM. `routing_terms` is how a product claims
|
|
562
|
-
// what its users call it, as opposed to what it contains, and unlike a summary it MAY
|
|
563
|
-
// introduce a term: that is the whole point of it, since the words most in need of
|
|
564
|
-
// routing — `flaky` — are the ones no index names an entity after.
|
|
565
|
-
for (const [product, index] of Object.entries(products)) {
|
|
566
|
-
for (const word of index.routing_terms ?? []) {
|
|
567
|
-
const key = fold(String(word));
|
|
568
|
-
if (!key)
|
|
569
|
-
continue;
|
|
570
|
-
if (!owners.has(key))
|
|
571
|
-
owners.set(key, new Set());
|
|
572
|
-
owners.get(key).add(product);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
// THE SUMMARY CAN ADD A CLAIMANT, NEVER A TERM.
|
|
576
|
-
//
|
|
577
|
-
// A product that models `view` as an entity and one that only describes views in its
|
|
578
|
-
// summary both answer questions about views, but only the first was visible here, so
|
|
579
|
-
// "delete the saved view" read as settled. Letting the summary claim a word the other
|
|
580
|
-
// product already models catches that, and refusing to mine NEW terms out of it is what
|
|
581
|
-
// keeps the prose from taking over: a summary is a paragraph of verbs and nouns written
|
|
582
|
-
// to sell the product, and every word in it would otherwise become routing vocabulary.
|
|
583
|
-
const summaries = Object.fromEntries(Object.entries(products).map(([product, index]) => [
|
|
584
|
-
product,
|
|
585
|
-
` ${fold(index.summary ?? "")} `,
|
|
586
|
-
]));
|
|
587
|
-
for (const [key, claimants] of owners) {
|
|
588
|
-
for (const [product, text] of Object.entries(summaries)) {
|
|
589
|
-
if (text.includes(` ${key} `))
|
|
590
|
-
claimants.add(product);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
542
|
const shared = new Map();
|
|
594
|
-
const decided = new Set();
|
|
595
|
-
const decidedBy = new Map();
|
|
596
543
|
for (const [key, claimants] of owners) {
|
|
597
544
|
if (!asked_.includes(` ${key} `))
|
|
598
545
|
continue;
|
|
599
|
-
// A
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (GENERIC_ANCHORS.has(key))
|
|
603
|
-
continue;
|
|
604
|
-
if (claimants.size < 2) {
|
|
605
|
-
// A term only one product claims POINTS somewhere; whether it decides the question
|
|
606
|
-
// depends on what else is in the sentence. It used to end the check outright, and
|
|
607
|
-
// that is what made this gate almost silent: an agent's paraphrase nearly always
|
|
608
|
-
// carries some incidental one-product word — `upload`, `saved report` — so the
|
|
609
|
-
// shared noun the user actually said stopped being examined. Worse, a sentence
|
|
610
|
-
// carrying one-product words for BOTH products read as settled, which is the case
|
|
611
|
-
// most in need of asking.
|
|
612
|
-
if (!GENERIC_ANCHORS.has(key)) {
|
|
613
|
-
const owner = [...claimants][0];
|
|
614
|
-
decided.add(owner);
|
|
615
|
-
decidedBy.set(owner, [...(decidedBy.get(owner) ?? []), key]);
|
|
616
|
-
}
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
546
|
+
// A term only one product claims decides the query outright.
|
|
547
|
+
if (claimants.size < 2)
|
|
548
|
+
return none;
|
|
619
549
|
shared.set(key, claimants);
|
|
620
550
|
}
|
|
621
|
-
// NOTHING RECOGNISED IS THE MOST AMBIGUOUS CASE, NOT THE LEAST.
|
|
622
|
-
//
|
|
623
|
-
// "Can we ship this?", "What should we fix first?", "Show me the failed tests." — no
|
|
624
|
-
// word in them is any product's, so the shared-term check finds nothing and used to
|
|
625
|
-
// report the same calm verdict it gives a request that genuinely settles itself. These
|
|
626
|
-
// are the requests with the least to go on, and answering them for a product nobody
|
|
627
|
-
// chose is exactly the guess this whole check exists to stop.
|
|
628
|
-
if (shared.size === 0 && decided.size === 0)
|
|
629
|
-
return { ...none, unknown: true };
|
|
630
|
-
// Decided, with no shared word anywhere in it — "bulk delete test cases". There is
|
|
631
|
-
// nothing to ask about, and saying WHICH product it landed on matters as much here as
|
|
632
|
-
// it does below: a caller that only hears "no clash" can still search the other one.
|
|
633
551
|
if (shared.size === 0)
|
|
634
|
-
return
|
|
635
|
-
? {
|
|
636
|
-
...none,
|
|
637
|
-
settled: [...decided][0],
|
|
638
|
-
because: (decidedBy.get([...decided][0]) ?? []).sort(),
|
|
639
|
-
}
|
|
640
|
-
: none;
|
|
641
|
-
// One product, and only one, is pointed at by the unshared words: the sentence answers
|
|
642
|
-
// itself and there is nothing to ask. It is still worth SAYING which one, because the
|
|
643
|
-
// caller sees only that nothing needs asking — "tell me about this project, including
|
|
644
|
-
// its settings" settles on the product that models settings, and an answer of silence
|
|
645
|
-
// let the other one be searched instead, which is the same wrong turn by a quieter route.
|
|
646
|
-
if (decided.size === 1)
|
|
647
|
-
return {
|
|
648
|
-
...none,
|
|
649
|
-
settled: [...decided][0],
|
|
650
|
-
because: (decidedBy.get([...decided][0]) ?? []).sort(),
|
|
651
|
-
};
|
|
552
|
+
return none;
|
|
652
553
|
const claimed = new Set();
|
|
653
554
|
for (const claimants of shared.values())
|
|
654
555
|
for (const product of claimants)
|
|
@@ -806,102 +707,3 @@ export function searchCapabilities(products, query, options = {}) {
|
|
|
806
707
|
weak: wanted.length > 0 && perfect > 0 && coverage < WEAK_COVERAGE,
|
|
807
708
|
};
|
|
808
709
|
}
|
|
809
|
-
/**
|
|
810
|
-
* Which products the USER'S OWN WORDS name, directly or through vocabulary only one
|
|
811
|
-
* product claims.
|
|
812
|
-
*
|
|
813
|
-
* This is the other half of `ambiguousProducts`. That one asks "is the query a word both
|
|
814
|
-
* products use?"; this asks "did the user say anything that settles it?" — and it reads
|
|
815
|
-
* the user's text, not a flag the caller sets about itself. A boolean saying the user
|
|
816
|
-
* confirmed is answered by whoever wants to proceed; a quote has to come from the
|
|
817
|
-
* conversation, and the words in it are checked against the index here.
|
|
818
|
-
*
|
|
819
|
-
* Anchors are of two kinds and both are needed. A product NAME ("test management",
|
|
820
|
-
* "observability") is the explicit case. A word only one product's vocabulary claims
|
|
821
|
-
* ("quality gate", "test plan") is the implicit one, and it is the common one: users say
|
|
822
|
-
* what they want, not which product owns it.
|
|
823
|
-
*/
|
|
824
|
-
export function productAnchors(products, text) {
|
|
825
|
-
const none = { products: [], terms: [] };
|
|
826
|
-
if (terms(text).length === 0)
|
|
827
|
-
return none;
|
|
828
|
-
const fold = (s) => terms(s).map(singular).join(" ");
|
|
829
|
-
const said = ` ${fold(text || "")} `;
|
|
830
|
-
const found = new Map();
|
|
831
|
-
const claim = (product, term) => {
|
|
832
|
-
if (!found.has(product))
|
|
833
|
-
found.set(product, new Set());
|
|
834
|
-
found.get(product).add(term);
|
|
835
|
-
};
|
|
836
|
-
// The product's own name and key, as a user would type them. `summary` is prose, not a
|
|
837
|
-
// name, so only the leading clause before ':' is used — "BrowserStack Test Management",
|
|
838
|
-
// "BrowserStack Test Reporting & Analytics (TRA, aka Test Observability)".
|
|
839
|
-
for (const [product, index] of Object.entries(products)) {
|
|
840
|
-
const names = [
|
|
841
|
-
product,
|
|
842
|
-
...(index.summary ?? "").split(":")[0].split(/[(),]/),
|
|
843
|
-
];
|
|
844
|
-
for (const name of names) {
|
|
845
|
-
// "aka Test Observability" is the product's other name with a connective in front
|
|
846
|
-
// of it; the user types the name, not the connective.
|
|
847
|
-
const key = fold(name.replace(/^\s*(browserstack|aka|formerly)\s+/i, ""));
|
|
848
|
-
if (key.length < 2)
|
|
849
|
-
continue;
|
|
850
|
-
if (said.includes(` ${key} `))
|
|
851
|
-
claim(product, key);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
// Vocabulary only one product claims. Shared words settle nothing and are skipped —
|
|
855
|
-
// that is exactly what `ambiguousProducts` flagged in the first place.
|
|
856
|
-
const owners = new Map();
|
|
857
|
-
for (const [product, entries] of Object.entries(vocabularyOf(products))) {
|
|
858
|
-
for (const entry of entries) {
|
|
859
|
-
for (const word of [entry.entity, ...(entry.aliases ?? [])]) {
|
|
860
|
-
const key = fold(String(word));
|
|
861
|
-
if (!key)
|
|
862
|
-
continue;
|
|
863
|
-
if (!owners.has(key))
|
|
864
|
-
owners.set(key, new Set());
|
|
865
|
-
owners.get(key).add(product);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
// The same words `ambiguousProducts` routes on, so a term that settles the question
|
|
870
|
-
// there also anchors the product here — otherwise `flaky` names a product in one half
|
|
871
|
-
// of the check and nothing at all in the other.
|
|
872
|
-
for (const [product, index] of Object.entries(products)) {
|
|
873
|
-
for (const word of index.routing_terms ?? []) {
|
|
874
|
-
const key = fold(String(word));
|
|
875
|
-
if (!key)
|
|
876
|
-
continue;
|
|
877
|
-
if (!owners.has(key))
|
|
878
|
-
owners.set(key, new Set());
|
|
879
|
-
owners.get(key).add(product);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
const matched = [];
|
|
883
|
-
for (const [key, claimants] of owners) {
|
|
884
|
-
if (claimants.size !== 1)
|
|
885
|
-
continue;
|
|
886
|
-
if (!said.includes(` ${key} `))
|
|
887
|
-
continue;
|
|
888
|
-
matched.push({ key, product: [...claimants][0] });
|
|
889
|
-
}
|
|
890
|
-
for (const { key, product } of matched) {
|
|
891
|
-
// A word carried by nearly every request in this domain anchors nothing: tra's
|
|
892
|
-
// sp_test lists the bare word `test` as an alias, which would make "what test plans
|
|
893
|
-
// are active" read as tra on the strength of one noun that means nothing on its own.
|
|
894
|
-
if (GENERIC_ANCHORS.has(key))
|
|
895
|
-
continue;
|
|
896
|
-
// Longest match wins. "test management" and "test" both match the same sentence, and
|
|
897
|
-
// only the longer one carries the user's meaning — counting both makes a settled
|
|
898
|
-
// question look ambiguous again.
|
|
899
|
-
if (matched.some((other) => other.key !== key && ` ${other.key} `.includes(` ${key} `)))
|
|
900
|
-
continue;
|
|
901
|
-
claim(product, key);
|
|
902
|
-
}
|
|
903
|
-
return {
|
|
904
|
-
products: [...found.keys()].sort(),
|
|
905
|
-
terms: [...new Set([...found.values()].flatMap((s) => [...s]))].sort(),
|
|
906
|
-
};
|
|
907
|
-
}
|
|
@@ -233,58 +233,19 @@ export interface PagingRule {
|
|
|
233
233
|
* harness's `{ env: … }` value sources must NOT cross the boundary into the artifact.
|
|
234
234
|
*/
|
|
235
235
|
export interface AuthScheme {
|
|
236
|
-
/**
|
|
237
|
-
|
|
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";
|
|
236
|
+
/** "apiKey" puts the rendered template in a header; "http" with scheme "basic" encodes it. */
|
|
237
|
+
type: "apiKey" | "http";
|
|
247
238
|
/** apiKey only. Header is the sole supported location — see `egress.authHeaders`. */
|
|
248
239
|
in?: "header" | "cookie" | "query";
|
|
249
240
|
/** apiKey only: the header name, e.g. "Api-Token". */
|
|
250
241
|
name?: string;
|
|
251
242
|
/** http only. */
|
|
252
243
|
scheme?: string;
|
|
253
|
-
/**
|
|
244
|
+
/** Defaults to `{username}:{access_key}`. */
|
|
254
245
|
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
246
|
}
|
|
273
247
|
export interface ProductIndex {
|
|
274
248
|
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
249
|
/**
|
|
289
250
|
* How to authenticate to this product. Absent means the historical default: the caller's
|
|
290
251
|
* credentials as `Api-Token: {username}:{access_key}`, which is what every shipped index
|
|
@@ -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_AGENT, 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_AGENT, 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_AGENT, 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_AGENT, 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_AGENT, GetSubTestPlanSchema.shape, {
|
|
570
570
|
title: "Get Sub Test Plan",
|
|
571
571
|
readOnlyHint: true,
|
|
572
572
|
openWorldHint: false,
|
|
@@ -8,10 +8,7 @@
|
|
|
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
|
|
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.
|
|
11
|
+
* predictable than an agent. Point at `askBrowserStackAI` only when NO tool here can.
|
|
15
12
|
*
|
|
16
13
|
* The one that matters most: 15 of the 17 Test Management tools require a project
|
|
17
14
|
* identifier and NONE of them accepts its absence, yet no tool in this server lists
|
|
@@ -38,21 +35,21 @@ export declare const NEEDS_TEST_PLAN_ID: string;
|
|
|
38
35
|
/**
|
|
39
36
|
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
40
37
|
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
41
|
-
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else.
|
|
42
|
-
* (
|
|
43
|
-
*
|
|
44
|
-
* finds no create, and reports the capability as absent — which is exactly what
|
|
45
|
-
* concluded.
|
|
38
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
|
|
39
|
+
* (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
|
|
40
|
+
* /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
|
|
41
|
+
* read tools, finds no create, and reports the capability as absent — which is exactly what
|
|
42
|
+
* a QA eval concluded.
|
|
46
43
|
*
|
|
47
44
|
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
48
|
-
* inviting the model to route plan work
|
|
49
|
-
* otherwise say to prefer a specific tool whenever one fits.
|
|
45
|
+
* inviting the model to route plan work to the agent generally, because the tool
|
|
46
|
+
* descriptions otherwise say to prefer a specific tool whenever one fits.
|
|
50
47
|
*
|
|
51
|
-
* Caveat worth knowing:
|
|
52
|
-
* completes on a client that can show a prompt. On one that cannot, the intended write
|
|
53
|
-
*
|
|
48
|
+
* Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
|
|
49
|
+
* only completes on a client that can show a prompt. On one that cannot, the intended write
|
|
50
|
+
* comes back in `needs_approval` instead of happening.
|
|
54
51
|
*/
|
|
55
|
-
export declare const
|
|
52
|
+
export declare const PLAN_WRITES_VIA_AGENT: string;
|
|
56
53
|
/** A build id comes from either build-lookup tool. */
|
|
57
54
|
export declare const NEEDS_BUILD_ID: string;
|
|
58
55
|
/**
|
|
@@ -8,10 +8,7 @@
|
|
|
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
|
|
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.
|
|
11
|
+
* predictable than an agent. Point at `askBrowserStackAI` only when NO tool here can.
|
|
15
12
|
*
|
|
16
13
|
* The one that matters most: 15 of the 17 Test Management tools require a project
|
|
17
14
|
* identifier and NONE of them accepts its absence, yet no tool in this server lists
|
|
@@ -24,8 +21,8 @@
|
|
|
24
21
|
*/
|
|
25
22
|
/** No tool lists projects, so this genuinely has to go to the agent. */
|
|
26
23
|
export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool here lists projects, so if you do not " +
|
|
27
|
-
|
|
28
|
-
"retry this tool with the identifier
|
|
24
|
+
'have one, call askBrowserStackAI with product "tm" and ask which projects exist, then ' +
|
|
25
|
+
"retry this tool with the identifier it returns.";
|
|
29
26
|
/** A sibling tool can produce the id — prefer it over the agent. */
|
|
30
27
|
export function needsIdFrom(idLabel, sourceTool) {
|
|
31
28
|
return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
|
|
@@ -37,30 +34,30 @@ export function needsIdFrom(idLabel, sourceTool) {
|
|
|
37
34
|
* "create me a project" through the agent before letting the tool run.
|
|
38
35
|
*/
|
|
39
36
|
export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier. Creating a folder inside an EXISTING project " +
|
|
40
|
-
"needs that project's identifier (PR-*); no tool here lists projects, so
|
|
41
|
-
|
|
37
|
+
"needs that project's identifier (PR-*); no tool here lists projects, so ask " +
|
|
38
|
+
'askBrowserStackAI with product "tm" for it.';
|
|
42
39
|
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
43
40
|
export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "listTestPlans");
|
|
44
41
|
/**
|
|
45
42
|
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
46
43
|
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
47
|
-
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else.
|
|
48
|
-
* (
|
|
49
|
-
*
|
|
50
|
-
* finds no create, and reports the capability as absent — which is exactly what
|
|
51
|
-
* concluded.
|
|
44
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
|
|
45
|
+
* (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
|
|
46
|
+
* /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
|
|
47
|
+
* read tools, finds no create, and reports the capability as absent — which is exactly what
|
|
48
|
+
* a QA eval concluded.
|
|
52
49
|
*
|
|
53
50
|
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
54
|
-
* inviting the model to route plan work
|
|
55
|
-
* otherwise say to prefer a specific tool whenever one fits.
|
|
51
|
+
* inviting the model to route plan work to the agent generally, because the tool
|
|
52
|
+
* descriptions otherwise say to prefer a specific tool whenever one fits.
|
|
56
53
|
*
|
|
57
|
-
* Caveat worth knowing:
|
|
58
|
-
* completes on a client that can show a prompt. On one that cannot, the intended write
|
|
59
|
-
*
|
|
54
|
+
* Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
|
|
55
|
+
* only completes on a client that can show a prompt. On one that cannot, the intended write
|
|
56
|
+
* comes back in `needs_approval` instead of happening.
|
|
60
57
|
*/
|
|
61
|
-
export const
|
|
62
|
-
|
|
63
|
-
"
|
|
58
|
+
export const PLAN_WRITES_VIA_AGENT = " Creating a test plan or sub-plan, and linking or unlinking test runs on one, are not " +
|
|
59
|
+
'available as tools here: call askBrowserStackAI with product "tm" and describe what you ' +
|
|
60
|
+
"want. It asks you to confirm before changing anything.";
|
|
64
61
|
/** A build id comes from either build-lookup tool. */
|
|
65
62
|
export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
|
|
66
63
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserstack/mcp-server",
|
|
3
|
-
"version": "2.2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "BrowserStack's Official MCP Server",
|
|
5
5
|
"mcpName": "io.github.browserstack/mcp-server",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
"test": "vitest run",
|
|
17
17
|
"lint": "eslint . --ext .ts",
|
|
18
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",
|
|
19
|
+
"check:contract": "python3 scripts/check-contract.py capability/tm.capability-index.json --baseline scripts/contract-baseline/tm.json --quiet && python3 scripts/check-contract.py capability/loadtesting.capability-index.json --baseline scripts/contract-baseline/loadtesting.json --quiet",
|
|
20
20
|
"eval:index": "tsx scripts/eval-index.mts",
|
|
21
21
|
"seed:live": "tsx scripts/seed-live.mts",
|
|
22
|
-
"seed:live:tra": "tsx scripts/seed-live-tra.mts",
|
|
23
22
|
"plan:tests": "tsx scripts/plan-capability-tests.mts",
|
|
24
23
|
"replay:live": "tsx scripts/replay-live.mts"
|
|
25
24
|
},
|
|
@@ -40,7 +39,7 @@
|
|
|
40
39
|
"node": ">=20.9.0"
|
|
41
40
|
},
|
|
42
41
|
"author": "",
|
|
43
|
-
"license": "
|
|
42
|
+
"license": "AGPL-3.0-only",
|
|
44
43
|
"dependencies": {
|
|
45
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
45
|
"@types/form-data": "^2.5.2",
|