@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.
Files changed (44) hide show
  1. package/README.md +42 -432
  2. package/capability/loadtesting.capability-index.json +25 -2
  3. package/capability/tm.capability-index.json +1 -2
  4. package/dist/config.d.ts +27 -2
  5. package/dist/config.js +19 -6
  6. package/dist/lib/instrumentation.js +29 -3
  7. package/dist/lib/utils.js +8 -0
  8. package/dist/tools/accessiblity-utils/scanner.js +1 -1
  9. package/dist/tools/ask-browserstack/central-oauth.d.ts +46 -25
  10. package/dist/tools/ask-browserstack/central-oauth.js +177 -55
  11. package/dist/tools/ask-browserstack/config.d.ts +0 -14
  12. package/dist/tools/ask-browserstack/config.js +0 -16
  13. package/dist/tools/ask-browserstack/deferred.d.ts +74 -0
  14. package/dist/tools/ask-browserstack/deferred.js +242 -0
  15. package/dist/tools/ask-browserstack/register.d.ts +3 -2
  16. package/dist/tools/ask-browserstack/register.js +219 -72
  17. package/dist/tools/ask-browserstack/relay.js +11 -17
  18. package/dist/tools/ask-browserstack/stream.d.ts +27 -5
  19. package/dist/tools/ask-browserstack/stream.js +25 -14
  20. package/dist/tools/ask-browserstack/types.d.ts +54 -3
  21. package/dist/tools/ask-browserstack/types.js +11 -1
  22. package/dist/tools/automate-utils/fetch-screenshots.js +17 -15
  23. package/dist/tools/capability-registry/bind.js +7 -43
  24. package/dist/tools/capability-registry/discovery.js +1 -3
  25. package/dist/tools/capability-registry/egress.d.ts +5 -12
  26. package/dist/tools/capability-registry/egress.js +1 -24
  27. package/dist/tools/capability-registry/redact.js +7 -5
  28. package/dist/tools/capability-registry/register.js +41 -289
  29. package/dist/tools/capability-registry/resolve.d.ts +1 -1
  30. package/dist/tools/capability-registry/resolve.js +3 -24
  31. package/dist/tools/capability-registry/search.d.ts +1 -28
  32. package/dist/tools/capability-registry/search.js +4 -202
  33. package/dist/tools/capability-registry/types.d.ts +3 -42
  34. package/dist/tools/testmanagement.js +5 -5
  35. package/dist/tools/tool-handoff.d.ts +12 -15
  36. package/dist/tools/tool-handoff.js +18 -21
  37. package/package.json +3 -4
  38. package/capability/tra.capability-index.json +0 -52074
  39. package/dist/lib/central-oauth.d.ts +0 -133
  40. package/dist/lib/central-oauth.js +0 -246
  41. package/dist/tools/capability-registry/oauth.d.ts +0 -63
  42. package/dist/tools/capability-registry/oauth.js +0 -176
  43. package/dist/tools/capability-registry/routing-token.d.ts +0 -25
  44. 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 word this domain uses in every other sentence settles nothing and clashes over
600
- // nothing: `test` is an alias on one side and appears throughout both summaries, so
601
- // counting it would make almost any request look like a clash.
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 decided.size === 1
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
- * "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";
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
- /** apiKey/http only. Defaults to `{username}:{access_key}`. */
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, PLAN_WRITES_VIA_CAPABILITIES, PROJECT_ID_ONLY_FOR_FOLDER, } from "./tool-handoff.js";
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
- PLAN_WRITES_VIA_CAPABILITIES, ListTestPlansSchema.shape, {
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
- PLAN_WRITES_VIA_CAPABILITIES, GetTestPlanSchema.shape, {
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
- PLAN_WRITES_VIA_CAPABILITIES, ListSubTestPlansSchema.shape, {
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
- PLAN_WRITES_VIA_CAPABILITIES, GetSubTestPlanSchema.shape, {
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 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.
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. 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.
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 elsewhere generally, because the tool descriptions
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: 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.
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 PLAN_WRITES_VIA_CAPABILITIES: string;
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 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.
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
- "have one, use the capability functions to list the Test Management projects, then " +
28
- "retry this tool with the identifier they return.";
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 use the " +
41
- "capability functions to look it up.";
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. 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.
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 elsewhere generally, because the tool descriptions
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: 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.
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 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.";
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-beta.1",
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": "ISC",
42
+ "license": "AGPL-3.0-only",
44
43
  "dependencies": {
45
44
  "@modelcontextprotocol/sdk": "^1.29.0",
46
45
  "@types/form-data": "^2.5.2",