From 7b699a5ece2ae4c91a767a61385b4cfebac9ace6 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 14 Oct 2026 17:39:15 +0530 Subject: [PATCH 1/7] fix(server): trim tool and parameter descriptions under client context caps Every client loads all tool descriptions at connect time. Kiro's power activation renders them into one tool result capped at 30,000 chars; ours was ~41,000, so Kiro truncated it to a 500-char preview and the agent guessed tool names that do not exist. Rewrites descriptions toward the documented budget (tool <=200 chars, param <=120): shared precondition constants, standalone description constants, and the capability-registry tools. Every routing rule is kept (listProducts first, ask on a clash, describe before invoke, weak_match re-search, '*' browse, paging, user_permission for writes, deletes refused), and every phrase pinned by tests is unchanged. Rendered size of the 44 hosted tools: 41,351 -> 27,512 chars. Co-Authored-By: Claude Opus 5.5 --- src/tools/accessibility.ts | 5 +- .../appautomate-utils/appium-sdk/constants.ts | 6 +- .../native-execution/constants.ts | 24 +--- src/tools/appautomate.ts | 10 +- src/tools/applive.ts | 10 +- src/tools/ask-browserstack/register.ts | 19 +-- src/tools/capability-registry/register.ts | 114 +++--------------- src/tools/get-failure-logs.ts | 2 +- src/tools/live.ts | 6 +- src/tools/rca-agent-utils/constants.ts | 8 +- src/tools/rca-agent.ts | 2 +- src/tools/sdk-utils/common/constants.ts | 2 +- src/tools/sdk-utils/common/schema.ts | 2 +- src/tools/selfheal.ts | 18 +-- .../testmanagement-utils/create-testcase.ts | 10 +- .../testmanagement-utils/list-testcases.ts | 4 +- .../testmanagement-utils/update-testcase.ts | 8 +- src/tools/testmanagement.ts | 28 ++--- src/tools/tool-handoff.ts | 26 ++-- 19 files changed, 75 insertions(+), 229 deletions(-) diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index 462c64dd..00d2d45e 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -577,8 +577,7 @@ export default function addAccessibilityTools( tools.getAccessibilityAuthConfig = server.tool( "getAccessibilityAuthConfig", - "Retrieve an existing authentication configuration by ID." + - NEEDS_A11Y_CONFIG_ID, + "Get an accessibility auth config by ID." + NEEDS_A11Y_CONFIG_ID, { configId: z.number().describe("ID of the auth configuration to retrieve"), }, @@ -596,7 +595,7 @@ export default function addAccessibilityTools( tools.fetchAccessibilityIssues = server.tool( "fetchAccessibilityIssues", - "Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results." + + "Fetch a completed scan's accessibility issues; page with `cursor`." + NEEDS_A11Y_SCAN_ID, { scanId: z diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index 5625709c..24d03d91 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -47,7 +47,7 @@ export const MobileDeviceSchema = z.object({ // Tool description and schema for setupBrowserStackAppAutomateTests export const SETUP_APP_AUTOMATE_DESCRIPTION = - "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead."; + "Set up the App Automate SDK for Appium tests. For pre-built Espresso or XCUITest suites use runAppTestsOnBrowserStack."; export const SETUP_APP_AUTOMATE_SCHEMA = { detectedFramework: z @@ -59,7 +59,7 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { detectedTestingFramework: z .nativeEnum(AppSDKSupportedTestingFrameworkEnum) .describe( - "The testing framework used in the project. Be precise with framework selection Example: 'testng', 'behave', 'pytest', 'robot'", + "Test framework in the project, e.g. testng, behave, pytest, robot.", ), detectedLanguage: z @@ -73,7 +73,7 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { .max(3) .default([]) .describe( - "Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]", + "Only devices the user asked for, e.g. [['android', 'Samsung Galaxy S24', '14']].", ), appPath: z diff --git a/src/tools/appautomate-utils/native-execution/constants.ts b/src/tools/appautomate-utils/native-execution/constants.ts index 58d5aa10..911b31a3 100644 --- a/src/tools/appautomate-utils/native-execution/constants.ts +++ b/src/tools/appautomate-utils/native-execution/constants.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { AppTestPlatform } from "./types.js"; -export const RUN_APP_AUTOMATE_DESCRIPTION = `Execute pre-built native mobile test suites (Espresso for Android, XCUITest for iOS) by direct upload to BrowserStack. ONLY for compiled .apk/.ipa test files. This is NOT for SDK integration or Appium tests. For Appium-based testing with SDK setup, use 'setupBrowserStackAppAutomateTests' instead.`; +export const RUN_APP_AUTOMATE_DESCRIPTION = `Run pre-built Espresso (Android) or XCUITest (iOS) suites on BrowserStack by upload. Not for Appium: use setupBrowserStackAppAutomateTests.`; export const MobileDeviceSchema = z.object({ platform: z @@ -19,35 +19,19 @@ export const RUN_APP_AUTOMATE_SCHEMA = { appPath: z .string() .describe( - "Path to your application file:\n" + - "If in development IDE directory:\n" + - "• For Android: 'gradle assembleDebug'\n" + - "• For iOS:\n" + - " xcodebuild clean -scheme YOUR_SCHEME && \\\n" + - " xcodebuild archive -scheme YOUR_SCHEME -configuration Release -archivePath build/app.xcarchive && \\\n" + - " xcodebuild -exportArchive -archivePath build/app.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist\n\n" + - "If in other directory, provide existing app path.\n" + - "The resolved file must be located inside the directory set in MCP_UPLOAD_BASE_DIR.", + "Built .apk/.ipa inside MCP_UPLOAD_BASE_DIR (gradle assembleDebug, or xcodebuild archive and export).", ), testSuitePath: z .string() .describe( - "Path to your test suite file:\n" + - "If in development IDE directory:\n" + - "• For Android: 'gradle assembleAndroidTest'\n" + - "• For iOS:\n" + - " xcodebuild test-without-building -scheme YOUR_SCHEME -destination 'generic/platform=iOS' && \\\n" + - " cd ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug-iphonesimulator/ && \\\n" + - " zip -r Tests.zip *.xctestrun *-Runner.app\n\n" + - "If in other directory, provide existing test file path.\n" + - "The resolved file must be located inside the directory set in MCP_UPLOAD_BASE_DIR.", + "Built suite inside MCP_UPLOAD_BASE_DIR (gradle assembleAndroidTest, or zipped .xctestrun + Runner.app).", ), devices: z .array(MobileDeviceSchema) .max(3) .default([]) .describe( - "Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]", + "Only devices the user asked for, e.g. [['android', 'Samsung Galaxy S24', '14']].", ), project: z .string() diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 2cc7e4ea..0cf1d2ae 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -315,13 +315,11 @@ export default function addAppAutomationTools( tools.takeAppScreenshot = server.tool( "takeAppScreenshot", - "Use this tool to take a screenshot of an app running on a BrowserStack device. This is useful for visual testing and debugging.", + "Take a screenshot of an app running on a BrowserStack device.", { desiredPhone: z .string() - .describe( - "The full name of the device to run the app on. Example: 'iPhone 12 Pro' or 'Samsung Galaxy S20'. Always ask the user for the device they want to use.", - ), + .describe("Exact device name, e.g. 'iPhone 15 Pro Max'. Ask the user."), desiredPlatformVersion: z .string() .describe( @@ -332,9 +330,7 @@ export default function addAppAutomationTools( .describe("Platform to run the app on. Either 'android' or 'ios'."), appPath: z .string() - .describe( - "The path to the .apk or .ipa file. Required for app installation. Must be located inside the directory set in MCP_UPLOAD_BASE_DIR.", - ), + .describe("Local .apk/.ipa path inside MCP_UPLOAD_BASE_DIR."), }, { title: "Take App Screenshot", diff --git a/src/tools/applive.ts b/src/tools/applive.ts index a8e159a3..15c91ffe 100644 --- a/src/tools/applive.ts +++ b/src/tools/applive.ts @@ -83,18 +83,16 @@ export default function addAppLiveTools( tools.runAppLiveSession = server.tool( "runAppLiveSession", - "Use this tool when user wants to manually check their app on a particular mobile device using BrowserStack's cloud infrastructure. Can be used to debug crashes, slow performance, etc.", + "Open an app on a real mobile device in App Live for a manual check or crash debugging.", { desiredPhone: z .string() .describe( - "The full name of the device to run the app on. Example: 'iPhone 12 Pro' or 'Samsung Galaxy S20' or 'Google Pixel 6'. Always ask the user for the device they want to use, do not assume it. ", + "Exact device name, e.g. 'iPhone 15 Pro Max'. Ask the user; never assume.", ), desiredPlatformVersion: z .string() - .describe( - "Specifies the platform version to run the app on. For example, use '12.0' for Android or '16.0' for iOS. If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'.", - ), + .describe("Version number, or 'latest' / 'oldest'."), desiredPlatform: z .enum(["android", "ios"]) .describe( @@ -103,7 +101,7 @@ export default function addAppLiveTools( appPath: z .string() .describe( - "The path to the .ipa or .apk file to install on the device. Always ask the user for the app path, do not assume it. Must be located inside the directory set in MCP_UPLOAD_BASE_DIR.", + "Local .apk/.ipa path inside MCP_UPLOAD_BASE_DIR. Ask the user; never assume.", ), }, { diff --git a/src/tools/ask-browserstack/register.ts b/src/tools/ask-browserstack/register.ts index 380a4c4c..d21e219f 100644 --- a/src/tools/ask-browserstack/register.ts +++ b/src/tools/ask-browserstack/register.ts @@ -133,19 +133,12 @@ const DESCRIPTION = // tool that is not enabled for the account cannot do the job at all — so "there is a // per-account gate, fall back to the individual tools" is the most useful thing to say // first. Parenthesised so it reads as a status note, not as the tool's purpose. - "(Alpha, limited availability. Enabled per account and per product; if it is not enabled " + - "the call returns an entitlement error, nothing runs, and you should complete the task " + - "with the individual tools instead. To request access, the user should contact their " + - "BrowserStack account owner.) " + - "Use this when no other BrowserStack tool here fits the task, or when the ones you tried " + - "did not get you there. Prefer a specific tool whenever one fits: it is faster and more " + - "predictable than handing the job to an agent. " + - "Otherwise, describe what you want in plain language and BrowserStack's agent decides " + - "which calls to make, then returns its answer plus the steps it took. Anything that would " + - "change data pauses and asks you to confirm it first; deletes are refused outright. " + - "Put that confirmation to the user and answer with what THEY say — never decide on " + - "their behalf. If it cannot be put to anyone, the run is read-only and everything it " + - "wanted to change comes back in `needs_approval` instead. One task per call."; + "(Alpha, limited availability. If not enabled it returns an entitlement error; complete " + + "the task with the individual tools instead.) Use this when no other BrowserStack tool " + + "here fits, or when the ones you tried did not get you there. Prefer a specific tool " + + "whenever one fits. Describe the task in plain language. A data change asks you to " + + "confirm first: put it to the user, never decide for them; deletes are refused " + + "outright. One task per call."; /** * `isError` marks a call that FAILED, not one that was refused. diff --git a/src/tools/capability-registry/register.ts b/src/tools/capability-registry/register.ts index e14942f8..0392374c 100644 --- a/src/tools/capability-registry/register.ts +++ b/src/tools/capability-registry/register.ts @@ -279,26 +279,13 @@ export function addCapabilityRegistryTools( tools.listProducts = server.tool( "listProducts", - "List the BrowserStack products this surface can reach: what each one does, the " + - "entities it models, and a line saying what each entity is. START HERE — " + - "searchCapability needs a product, and this is what tells you which one. If two " + - "products could both fit the task, ask the user rather than choosing for them. " + - "ALWAYS pass the user's request, in their own words, as `query`: the answer then " + - "says outright whether the products clash over the words in it. WHEN IT REPORTS A " + - "CLASH — two products claiming the same word for different things — ASK THE USER " + - "WHICH PRODUCT THEY MEAN AND WAIT FOR THEIR ANSWER BEFORE SEARCHING OR INVOKING " + - "ANYTHING. The response carries the question to put to them and what the shared " + - "word means in each product. Choosing for them, or searching each product in turn " + - "and merging the results, is the failure this call exists to prevent.", + "List BrowserStack products and their entities. START HERE: searchCapability needs a product name from this. Pass the user's request as `query`; if it reports a clash, ask the user which product they mean and wait. Never pick for them or merge products.", { query: z .string() .optional() .describe( - "The user's request, in their own words. With it the response carries a " + - "`routing` block saying whether the products share the vocabulary this " + - "request uses — and, when they do, the question to put to the user before " + - "searching either product.", + "The user's request, in their own words; enables clash detection.", ), }, { @@ -461,9 +448,7 @@ export function addCapabilityRegistryTools( tools.describeEntity = server.tool( "describeEntity", - "Describe one entity: what it is, what identifies it, what it relates to, and the " + - "vocabulary the product uses for it. Read this before filtering or writing, because " + - "ids and field values usually have to be resolved first.", + "Describe an entity: identifiers, relations and product vocabulary. Read before filtering or writing; ids often need resolving first.", { product: productArg().describe( `Which product the entity belongs to: ${productList}.`, @@ -500,41 +485,11 @@ export function addCapabilityRegistryTools( tools.searchCapability = server.tool( "searchCapability", - "Find endpoints this surface can call, by plain language, within ONE product. " + - "CALL listProducts FIRST, passing the user's request as its `query`: it names the " + - "products, says whether they clash over the words in that request, and gives you " + - "the question to ask when they do. You need it anyway — `product` here takes a name " + - "exactly as listProducts spells it. Where two products use the same word for " + - "different things, ask the USER and wait for an answer rather than picking. " + - "Search matches the product's OWN words, not synonyms. When your words are not the " + - "product's, the response says so: `weak_match: true` with a `suggested_vocabulary` " + - "map of the product's entities and their aliases. Results are still returned, but " + - "treat them as unconfirmed — pick the closest entity from that map and search again " + - "using its vocabulary, or call describeEntity on it for the fuller picture. That one " + - "extra round trip is far cheaper than invoking the wrong capability. " + - "Narrowing further with `entity` sharpens results. " + - "THIS IS A SHORTLIST, NOT A CONTRACT. Each result carries only what you need to " + - "CHOOSE: `name` (the handle), `product`, `mode` (whether it writes), `intent` and " + - "`guidance` (what it does and what goes wrong), and `method`/`path` for products " + - "that publish no name yet. " + - "It does NOT carry parameters or response shapes. Once you have picked one, call " + - "describeCapability for its full contract, then invokeCapability. Fetching the " + - "contract only for the one you chose is the difference between ~1k and ~8.6k tokens " + - "a search. Results are ranked and capped. " + - "TO SEE EVERYTHING rather than search, pass query='*' — that lists the product " + - "(honouring `entity` and `mode`) in a stable order instead of ranking it, which is " + - "what you want when the task is 'what can this product do' rather than a lookup. " + - "TO PAGE, send `offset`: every response carries `offset` and, when more matched, " + - "`next_offset` — resend the SAME query with that value. `next_offset` is absent on " + - "the last page, so walk until it stops rather than comparing counts.", + "Find capabilities in ONE product by plain language. CALL listProducts FIRST; `product` must be spelled exactly as listProducts spells it. If `weak_match` is true, search again with `suggested_vocabulary` before invoking. Results are a shortlist: call describeCapability for the contract, then invokeCapability. query='*' lists all; page with `next_offset`.", { query: z .string() - .describe( - "What you are trying to do, in plain language. Pass '*' (or 'all') to BROWSE " + - "instead of search: every capability in the product, in a stable order, " + - "paginated — use it when you want to see the surface rather than find one thing.", - ), + .describe("Goal in plain language, or '*' to list every capability."), entity: z .string() .optional() @@ -562,11 +517,7 @@ export function addCapabilityRegistryTools( // refused here instead, by a message that names listProducts. product: z .string() - .describe( - "Which product to search, named exactly as listProducts spells it. Call " + - "listProducts with the user's request to get the name — and to find out " + - "whether the products clash over it, in which case ask the user first.", - ), + .describe("Product name, exactly as listProducts spells it."), // `product_choice` USED TO LIVE HERE, AND WAS ITS OWN DEFEAT. // // A required enum of 'user_confirmed' | 'not_asked' publishes, in the schema every @@ -580,20 +531,13 @@ export function addCapabilityRegistryTools( .string() .optional() .describe( - "The `routing_token` from the last listProducts call or product_ambiguous " + - "refusal — it carries what that step concluded about the user's request, " + - "signed. Always pass the most recent one. When it reports a clash, send the " + - "user's answer in `user_words` alongside it; neither works alone.", + "Latest `routing_token` from listProducts or a clash refusal; send with `user_words` after a clash.", ), user_words: z .string() .optional() .describe( - "The USER'S OWN words, verbatim, that settle which product this is — either " + - "the product's name or a term only one product uses ('quality gate', 'test " + - "plan'). Quote them; do not paraphrase, and do not write words the user did " + - "not say. When a query could mean either product this is what lifts the " + - "refusal, because it is checked against the index rather than taken on trust.", + "The user's own words, verbatim, naming the product or a term only it uses. Never paraphrase.", ), mode: z .enum(["read", "write", "destructive"]) @@ -603,10 +547,7 @@ export function addCapabilityRegistryTools( offset: z .number() .optional() - .describe( - "Skip this many results — send the `next_offset` from the previous response " + - "with the SAME query to get the next page. Omit for the first page.", - ), + .describe("`next_offset` from the previous page, with the same query."), }, { title: "Search Capabilities", @@ -911,16 +852,7 @@ export function addCapabilityRegistryTools( tools.describeCapability = server.tool( "describeCapability", - "The full contract for ONE capability you picked from searchCapability: its " + - "parameters grouped into path_params / query / body under the spec's own names, " + - "what it returns, and its declared response shapes fully expanded. Call this after " + - "search and before invokeCapability — search deliberately omits all of it, because " + - "it is only needed for the one capability you actually call. " + - "Identify it by `name`, exactly as search returned it; only when a result carries " + - "no `name` (some products publish none yet) pass `method` and `path` instead. " + - "Every parameter lists its type, whether it is required, its allowed values where " + - "the set is closed, and any limits the product declares — obey those before calling " + - "rather than discovering them from a rejected request.", + "Full contract (parameters, returns, response shapes) for ONE capability from searchCapability. Call it before invokeCapability. Identify by `name`; use `method` + `path` only when no name was returned.", { name: z .string() @@ -942,17 +874,12 @@ export function addCapabilityRegistryTools( ), product: productArg() .optional() - .describe( - `Which product owns it (${productList}). searchCapability returns it on every ` + - "result; required only when two products share a name or a path.", - ), + .describe("Owning product; needed only when names or paths collide."), include_responses: z .enum(["success", "all", "none"]) .optional() .describe( - "Which declared responses to expand: 'success' (default, the 2xx shape), 'all' " + - "(adds the error shapes — several times larger, and near-identical across " + - "endpoints), or 'none'.", + "'success' (default), 'all' (adds error shapes, larger) or 'none'.", ), }, { @@ -1049,17 +976,7 @@ export function addCapabilityRegistryTools( tools.invokeCapability = server.tool( "invokeCapability", - "Call a capability whose contract you have from describeCapability. Pass `name` exactly " + - "as given — that is the handle. Only when a result carries no `name` (some products " + - "do not publish them yet) pass `method` and `path` instead, exactly as returned. " + - "Arguments go in path_params / query / body under the spec's own names. One call " + - "makes exactly one request and returns the product's own response untouched; when " + - "`completed` is false there is another page, which you fetch by sending the " + - "capability's own page parameter. If the mode is 'write' you MUST ask the user " + - "first, then resend with user_permission='granted' and a change_summary; both are " + - "recorded. Capabilities whose mode is 'destructive' (deletes) are refused outright — " + - "archiving, closing and merging are ordinary writes and DO run, so read the mode and " + - "intent before confirming with the user.", + "Run a capability after describeCapability, by `name` (or `method` + `path` if unnamed); args go in path_params / query / body. If `completed` is false, fetch the next page. Writes: ask the user first, then resend with user_permission='granted' and a change_summary. Deletes are refused; archive, close and merge are writes.", { name: z .string() @@ -1094,10 +1011,7 @@ export function addCapabilityRegistryTools( .describe("Body fields, under the spec's names."), product: productArg() .optional() - .describe( - `Which product owns the capability (${productList}). searchCapability returns it ` + - "on every result; required only when two products share a name or a path.", - ), + .describe("Owning product; needed only when names or paths collide."), user_permission: z .enum(PERMISSION_VALUES) .optional() diff --git a/src/tools/get-failure-logs.ts b/src/tools/get-failure-logs.ts index 267830d2..b0e60141 100644 --- a/src/tools/get-failure-logs.ts +++ b/src/tools/get-failure-logs.ts @@ -188,7 +188,7 @@ export default function registerGetFailureLogs( tools.getFailureLogs = server.tool( "getFailureLogs", - "Fetch logs, or the session video URL, for an Automate/App Automate session." + + "Fetch logs or the video URL for an Automate/App Automate session." + NEEDS_SESSION_ID, { sessionType: z diff --git a/src/tools/live.ts b/src/tools/live.ts index fa1ac4bc..cd49e201 100644 --- a/src/tools/live.ts +++ b/src/tools/live.ts @@ -21,7 +21,7 @@ const LiveArgsShape = { desiredOSVersion: z .string() .describe( - "The OS version must be specified as a version number (e.g., '10', '14.0') or as a keyword such as 'latest' or 'oldest'. Normalize variations like 'newest' or 'most recent' to 'latest', and terms like 'earliest' or 'first' to 'oldest'. For macOS, version names (e.g., 'Sequoia') must be used instead of numeric versions.", + "Version number or 'latest'/'oldest'; macOS takes names, e.g. 'Sequoia'.", ), desiredBrowser: z .enum(["chrome", "firefox", "safari", "edge", "ie"]) @@ -29,9 +29,7 @@ const LiveArgsShape = { desiredBrowserVersion: z .string() .optional() - .describe( - "Browser version for desktop (e.g. '133.2', 'latest'). If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'.", - ), + .describe("Desktop browser version, or 'latest' / 'oldest'."), desiredDevice: z.string().optional().describe("Device name for mobile"), }; diff --git a/src/tools/rca-agent-utils/constants.ts b/src/tools/rca-agent-utils/constants.ts index 9ecbfebf..d0ace5f9 100644 --- a/src/tools/rca-agent-utils/constants.ts +++ b/src/tools/rca-agent-utils/constants.ts @@ -10,9 +10,7 @@ export const FETCH_RCA_PARAMS = { testId: z .array(z.number().int()) .max(3) - .describe( - "Array of integer test IDs to fetch RCA data for (maximum 3 IDs). These must be numeric test IDs, not session IDs or strings. If not provided, use the listTestIds tool to get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed.", - ), + .describe("Up to 3 numeric test ids from listTestIds (not session ids)."), }; export const GET_BUILD_ID_PARAMS = { @@ -31,9 +29,7 @@ export const GET_BUILD_ID_PARAMS = { export const LIST_TEST_IDS_PARAMS = { buildId: z .string() - .describe( - "The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name", - ), + .describe("Build id; get it with getBuildId or listBuildId."), status: z .nativeEnum(TestStatus) .describe( diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 7a7dc922..3eb2b5e7 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -194,7 +194,7 @@ export default function addRCATools( tools.fetchRCA = server.tool( "fetchRCA", - "Fetch AI Root Cause Analysis for the current user's failed BrowserStack Automate/App-Automate tests. Suggests fixes only; never auto-apply, require explicit user approval." + + "AI root cause for failed Automate/App Automate tests. Suggests fixes only; never auto-apply, require explicit user approval." + NEEDS_TEST_IDS, FETCH_RCA_PARAMS, { diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts index 30a68f76..a5d9d683 100644 --- a/src/tools/sdk-utils/common/constants.ts +++ b/src/tools/sdk-utils/common/constants.ts @@ -12,7 +12,7 @@ export const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = "Adds Percy snapshot commands to the specified test files."; export const RUN_ON_BROWSERSTACK_DESCRIPTION = - "Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use this tool for functional or integration test setup on BrowserStack only. For any visual testing or Percy integration, use the dedicated Percy setup tool. Example prompts: run this test on browserstack; set up this project for browserstack."; + "Set up and run web tests on BrowserStack Automate with the SDK (functional or integration). For Percy visual testing use the Percy tool."; export const SIMULATE_PERCY_CHANGE_DESCRIPTION = `Integrate Percy visual testing into new projects and demonstrate visual change detection through a step-by-step simulation. This tool handles initial Percy integration, setup, and creates a working demonstration for new users. Primary tool for prompts like: "Integrate percy for this project", "Integrate percy in this project {project_name}"`; diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 1cb6159b..483fdff1 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -88,7 +88,7 @@ export const RunTestsOnBrowserStackParamsShape = { .max(3) .default([]) .describe( - "Device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'windows', osVersion: '11', browser: 'chrome', browserVersion: 'latest' }] or [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14', browser: 'chrome' }].", + "Only devices the user asked for, e.g. [{ platform: 'windows', osVersion: '11', browser: 'chrome', browserVersion: 'latest' }].", ), }; diff --git a/src/tools/selfheal.ts b/src/tools/selfheal.ts index 0795cb48..e74000b5 100644 --- a/src/tools/selfheal.ts +++ b/src/tools/selfheal.ts @@ -568,11 +568,7 @@ export default function addSelfHealTools( tools.fetchSelfHealedSelectors = server.tool( "fetchSelfHealedSelectors", - "Fetch BrowserStack self-healed selectors plus the test source code for " + - "the run. Provide exactly one of `sessionId` (single Automate / " + - "App-Automate session) or `buildUuid` (full self-healing report for a " + - "build). Pass the returned locator pairs to `prepareSelfHealingPlan` " + - "to plan edits." + + "Fetch self-healed selectors and the test source. Pass exactly one of `sessionId` or `buildUuid`; give the pairs to prepareSelfHealingPlan." + NEEDS_SESSION_ID, { sessionId: z @@ -653,17 +649,7 @@ export default function addSelfHealTools( tools.prepareSelfHealingPlan = server.tool( "prepareSelfHealingPlan", - "Build a self-healing edit plan that bundles locator pairs with test " + - "source code for the calling LLM to apply with its own editing tools. " + - "This tool does NOT modify files — it returns structured context so " + - "the LLM can edit only the relevant call sites (avoids blind " + - "find/replace across shared ids/classes). `sessions` accepts the " + - "canonical shape `[{sessionId, locators: [{original, healed, " + - "thought?}]}]`, a single session object, an envelope `{sessions: " + - "[...]}`, the raw report `{healing_logs: [...]}` (with " + - "`healed_selectors` aliasing `locators`), and snake_case keys " + - "(`session_id`, `original_locator`, `healed_locator`, " + - "`healing_thought`)." + + "Build an edit plan from healed locator pairs plus test source; it does NOT modify files. `sessions`: [{sessionId, locators: [{original, healed}]}] or the raw healing report." + NEEDS_SESSION_ID, { sessions: sessionsFieldSchema.describe( diff --git a/src/tools/testmanagement-utils/create-testcase.ts b/src/tools/testmanagement-utils/create-testcase.ts index c87a2c01..eafff88a 100644 --- a/src/tools/testmanagement-utils/create-testcase.ts +++ b/src/tools/testmanagement-utils/create-testcase.ts @@ -90,14 +90,10 @@ export const customFieldValueSchema = z.union([ export const CreateTestCaseSchema = z.object({ project_identifier: z .string() - .describe( - "The ID of the BrowserStack project where the test case should be created. If no project identifier is provided, ask the user if they would like to create a new project using the createProjectOrFolder tool.", - ), + .describe("Project id (PR-*). If none, offer createProjectOrFolder."), folder_id: z .string() - .describe( - "The ID of the folder within the project where the test case should be created. If not provided, ask the user if they would like to create a new folder using the createProjectOrFolder tool.", - ), + .describe("Folder id. If none, offer createProjectOrFolder."), name: z.string().describe("Name of the test case."), description: z .string() @@ -158,7 +154,7 @@ export const CreateTestCaseSchema = z.object({ .string() .optional() .describe( - "Priority of the test case. Accepts either display name (e.g. 'Critical', 'High', 'Medium', 'Low') or internal name (e.g. 'medium'). If omitted, the project default (usually 'Medium') is applied. Valid values are per-project and discoverable via the form-fields endpoint.", + "Priority, e.g. 'High' or 'medium'; values vary per project; default Medium.", ), case_type: z .string() diff --git a/src/tools/testmanagement-utils/list-testcases.ts b/src/tools/testmanagement-utils/list-testcases.ts index 67d5d4ff..a47e5fbe 100644 --- a/src/tools/testmanagement-utils/list-testcases.ts +++ b/src/tools/testmanagement-utils/list-testcases.ts @@ -18,9 +18,7 @@ export const ListTestCasesSchema = z.object({ folder_id: z .string() .optional() - .describe( - "Optional. If provided, only return test cases in this folder. If omitted, returns all test cases in the project. Folder ids can be discovered via listFolders.", - ), + .describe("Only this folder's cases (ids from listFolders). Omit for all."), case_type: z .string() .optional() diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts index 0b7a1cbf..05383993 100644 --- a/src/tools/testmanagement-utils/update-testcase.ts +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -73,20 +73,18 @@ export const UpdateTestCaseSchema = z.object({ priority: z .string() .optional() - .describe( - "Updated priority. Accepts either display name (e.g. 'Medium', 'Critical', 'High', 'Low') or internal name (e.g. 'medium'). Valid values are per-project and discoverable via the form-fields endpoint.", - ), + .describe("Priority, e.g. 'High' or 'medium'; values vary per project."), case_type: z .string() .optional() .describe( - "Updated test case type. Accepts either display name (e.g. 'Functional', 'Regression', 'Smoke & Sanity') or internal name (e.g. 'functional', 'smoke_sanity'). Valid values are per-project.", + "Type, e.g. 'Functional' or 'smoke_sanity'; values vary per project.", ), automation_status: z .string() .optional() .describe( - "Updated automation status. Use internal name such as 'not_automated', 'automated', 'automation_not_required', 'cannot_be_automated', or 'obsolete'.", + "e.g. not_automated, automated, automation_not_required, obsolete.", ), status: z .string() diff --git a/src/tools/testmanagement.ts b/src/tools/testmanagement.ts index 6b47948c..35189cf4 100644 --- a/src/tools/testmanagement.ts +++ b/src/tools/testmanagement.ts @@ -755,8 +755,7 @@ export default function addTestManagementTools( tools.createTestCase = server.tool( "createTestCase", - "Use this tool to create a test case in BrowserStack Test Management." + - NEEDS_PROJECT_ID, + "Create a test case in BrowserStack Test Management." + NEEDS_PROJECT_ID, CreateTestCaseSchema.shape, { title: "Create Test Case", @@ -770,7 +769,7 @@ export default function addTestManagementTools( tools.updateTestCase = server.tool( "updateTestCase", - "Update an existing test case in BrowserStack Test Management. Any subset of the following fields may be changed: name, description, preconditions, test_case_steps, owner, priority, case_type, automation_status, status, tags, issues, custom_fields. Only the supplied fields are modified." + + "Update an existing test case; only the supplied fields change." + NEEDS_PROJECT_ID, UpdateTestCaseSchema.shape, { @@ -785,7 +784,7 @@ export default function addTestManagementTools( tools.listTestCases = server.tool( "listTestCases", - "List test cases in a project, optionally scoped to a specific folder. Omit folder_id to list all test cases in the project; provide folder_id (discoverable via listFolders) to list only that folder's cases. Supports filters: case_type, priority, pagination." + + "List test cases in a project, optionally one folder (see listFolders). Filters: case_type, priority, paging." + NEEDS_PROJECT_ID, ListTestCasesSchema.shape, { @@ -800,7 +799,7 @@ export default function addTestManagementTools( tools.listFolders = server.tool( "listFolders", - "List folders in a BrowserStack Test Management project, returning each folder's id and name (plus case counts and sub-folder counts). Pass parent_id to list sub-folders under a specific folder instead of top-level folders." + + "List folders (id, name, counts) in a project; parent_id lists sub-folders." + NEEDS_PROJECT_ID, ListFoldersSchema.shape, { @@ -843,8 +842,7 @@ export default function addTestManagementTools( tools.listTestRuns = server.tool( "listTestRuns", - "List test runs in a project with optional filters (date ranges, assignee, state, etc.)" + - NEEDS_PROJECT_ID, + "List test runs with filters (dates, assignee, state)." + NEEDS_PROJECT_ID, ListTestRunsSchema.shape, { title: "List Test Runs", @@ -858,8 +856,7 @@ export default function addTestManagementTools( tools.updateTestRun = server.tool( "updateTestRun", - "Update a test run's metadata and/or add test cases to it." + - NEEDS_PROJECT_ID, + "Update a test run or add test cases to it." + NEEDS_PROJECT_ID, UpdateTestRunSchema.shape, { title: "Update Test Run", @@ -873,8 +870,7 @@ export default function addTestManagementTools( tools.addTestResult = server.tool( "addTestResult", - "Add a test result to a specific test run via BrowserStack Test Management API." + - NEEDS_PROJECT_ID, + "Add a test result to a test run." + NEEDS_PROJECT_ID, AddTestResultSchema.shape, { title: "Add Test Result", @@ -918,7 +914,7 @@ export default function addTestManagementTools( tools.createLCASteps = server.tool( "createLCASteps", - "Generate Low Code Automation (LCA) steps for a test case in BrowserStack Test Management using the Low Code Automation Agent." + + "Generate Low Code Automation (LCA) steps for a test case." + NEEDS_PROJECT_ID, CreateLCAStepsSchema.shape, { @@ -933,7 +929,7 @@ export default function addTestManagementTools( 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." + + "List test plans (TP-*) with status, dates and run counts; paginated." + NEEDS_PROJECT_ID + PLAN_WRITES_VIA_CAPABILITIES, ListTestPlansSchema.shape, @@ -949,7 +945,7 @@ export default function addTestManagementTools( 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." + + "Get a test plan (TP-*): metadata, linked runs, case count and status summary." + NEEDS_PROJECT_ID + NEEDS_TEST_PLAN_ID + PLAN_WRITES_VIA_CAPABILITIES, @@ -966,7 +962,7 @@ export default function addTestManagementTools( tools.listSubTestPlans = server.tool( "listSubTestPlans", - "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination." + + "List sub-plans under a test plan (TP-*); paginated." + NEEDS_PROJECT_ID + NEEDS_TEST_PLAN_ID + PLAN_WRITES_VIA_CAPABILITIES, @@ -983,7 +979,7 @@ export default function addTestManagementTools( tools.getSubTestPlan = server.tool( "getSubTestPlan", - "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs." + + "Get a sub-plan (STP-*): metadata and linked runs." + NEEDS_PROJECT_ID + NEEDS_TEST_PLAN_ID + PLAN_WRITES_VIA_CAPABILITIES, diff --git a/src/tools/tool-handoff.ts b/src/tools/tool-handoff.ts index ce41a2b3..fe697e7f 100644 --- a/src/tools/tool-handoff.ts +++ b/src/tools/tool-handoff.ts @@ -25,9 +25,7 @@ /** No tool lists projects, so this genuinely has to go to the agent. */ export const NEEDS_PROJECT_ID = - " Requires a project identifier (PR-*). No tool here lists projects, so if you do not " + - "have one, use the capability functions to list the Test Management projects, then " + - "retry this tool with the identifier they return."; + " Needs a project id (PR-*); if you lack one, get it from the capability functions."; /** A sibling tool can produce the id — prefer it over the agent. */ export function needsIdFrom(idLabel: string, sourceTool: string): string { @@ -41,9 +39,8 @@ export function needsIdFrom(idLabel: string, sourceTool: string): string { * "create me a project" through the agent before letting the tool run. */ export const PROJECT_ID_ONLY_FOR_FOLDER = - " Creating a project needs no identifier. Creating a folder inside an EXISTING project " + - "needs that project's identifier (PR-*); no tool here lists projects, so use the " + - "capability functions to look it up."; + " Creating a project needs no identifier; a folder inside an EXISTING project needs " + + "its id (PR-*), from the capability functions."; /** A test plan id (TP-*) comes from listTestPlans. */ export const NEEDS_TEST_PLAN_ID = needsIdFrom( @@ -69,9 +66,8 @@ export const NEEDS_TEST_PLAN_ID = needsIdFrom( * refused rather than performed. */ export const PLAN_WRITES_VIA_CAPABILITIES = - " Creating a test plan or sub-plan, and linking or unlinking test runs on one, are not " + - "available as tools here: use the capability functions, which reach Test Management's " + - "own API for these operations. They ask you to confirm before changing anything."; + " Creating a test plan or sub-plan, and linking or unlinking test runs, are not " + + "available as tools here: use the capability functions (they ask you to confirm first)."; /** A build id comes from either build-lookup tool. */ export const NEEDS_BUILD_ID = needsIdFrom( @@ -85,19 +81,17 @@ export const NEEDS_BUILD_ID = needsIdFrom( * pointing at the agent when a real tool exists is exactly what the header above forbids. */ export const NEEDS_SESSION_ID = - " Requires a session id. listSessions lists them for a build; if you do not have the " + - "build either, getBuildId or listBuildId resolves one from a project and build name."; + " Needs a session id: listSessions lists them; getBuildId or listBuildId finds the build."; /** A completed scan's ids come from startAccessibilityScan, or from the agent. */ export const NEEDS_A11Y_SCAN_ID = - " Requires the ids of a completed scan, which are returned by startAccessibilityScan. " + - "No tool here lists past scans, so if you do not have the ids, start a new scan rather " + - "than guessing."; + " Needs a completed scan's ids from startAccessibilityScan; past scans are not " + + "listed, so start a new scan rather than guessing."; /** Auth-config ids are not listable by any tool here. */ export const NEEDS_A11Y_CONFIG_ID = - " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " + - "existing configurations, so if you do not have the id, create one rather than guessing."; + " Needs the id from createAccessibilityAuthConfig; configs are not listed, so create " + + "one rather than guessing."; /** Test ids come from listTestIds, which itself needs a build id. */ export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds"); From 70857e992bc02ebc07fde134ba47923d6953c449 Mon Sep 17 00:00:00 2001 From: Shreyas Sarve Date: Wed, 21 Oct 2026 16:10:20 +0530 Subject: [PATCH 2/7] fix(server): trim Percy and test-management parameter descriptions Shorten Percy setup/scan parameter text and the createTestCase / updateTestCase parameter text without changing their meaning. Saves ~500 tokens on the 52-tool list. Co-Authored-By: Claude Sonnet 5.5 --- src/tools/sdk-utils/common/constants.ts | 4 +-- src/tools/sdk-utils/common/schema.ts | 12 ++++----- .../testmanagement-utils/create-testcase.ts | 26 +++++++++---------- .../testmanagement-utils/update-testcase.ts | 24 ++++++++--------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts index a5d9d683..96e2b7c5 100644 --- a/src/tools/sdk-utils/common/constants.ts +++ b/src/tools/sdk-utils/common/constants.ts @@ -3,7 +3,7 @@ export const IMPORTANT_SETUP_WARNING = "If you cannot run commands or edit files in the user's project (e.g. a chat-only client), present every step below to the user in full — including each shell command, the package.json changes, and the complete browserstack.yml contents — instead of summarizing them."; export const SETUP_PERCY_DESCRIPTION = - "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate. Example prompts: Expand percy coverage for this project {project_name}"; + "Set up or expand Percy visual testing coverage in a project, with or without existing Percy (Percy Web and Percy Automate)."; export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories."; @@ -14,7 +14,7 @@ export const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = export const RUN_ON_BROWSERSTACK_DESCRIPTION = "Set up and run web tests on BrowserStack Automate with the SDK (functional or integration). For Percy visual testing use the Percy tool."; -export const SIMULATE_PERCY_CHANGE_DESCRIPTION = `Integrate Percy visual testing into new projects and demonstrate visual change detection through a step-by-step simulation. This tool handles initial Percy integration, setup, and creates a working demonstration for new users. Primary tool for prompts like: "Integrate percy for this project", "Integrate percy in this project {project_name}"`; +export const SIMULATE_PERCY_CHANGE_DESCRIPTION = `Integrate Percy visual testing into a new project and demo visual change detection step by step. Primary tool for "Integrate percy in this project".`; export const PERCY_REPLACE_REGEX = /Invoke listTestFiles\(\) with the provided directories[\s\S]*?- DO NOT STOP until you add commands in all the files or you reach end of the files\./; diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 483fdff1..50e0ae11 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -23,7 +23,7 @@ export const MacOSPlatformEnum = { } as const; export const SetUpPercyParamsShape = { - projectName: z.string().describe("A unique name for your Percy project."), + projectName: z.string().describe("Unique Percy project name."), detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum), detectedBrowserAutomationFramework: z.nativeEnum( SDKSupportedBrowserAutomationFrameworkEnum, @@ -32,19 +32,19 @@ export const SetUpPercyParamsShape = { integrationType: z .nativeEnum(PercyIntegrationTypeEnum) .describe( - "Specify the Percy integration type: web (Percy Web) or automate (Percy Automate). If not provided, always prompt the user with: 'Please specify the Percy integration type.' Do not proceed without an explicit selection. Never use a default.", + "'web' (Percy Web) or 'automate' (Percy Automate). If unspecified, ask the user; never default.", ), folderPaths: z .array(z.string()) .optional() .describe( - "An array of absolute folder paths containing UI test files. If not provided, analyze codebase for UI test folders by scanning for test patterns which contain UI test cases as per framework. Return empty array if none found.", + "Absolute paths of folders with UI tests. If omitted, scan the codebase for them; [] if none.", ), filePaths: z .array(z.string()) .optional() .describe( - "An array of absolute file paths to specific UI test files. Use this when you want to target specific test files rather than entire folders. If not provided, will use folderPaths instead.", + "Absolute paths of specific UI test files, instead of folders. If omitted, folderPaths is used.", ), }; @@ -109,12 +109,12 @@ export const RunPercyScanParamsShape = { .string() .optional() .describe( - "The test command to run with Percy. Optional — the LLM should try to infer it first from project context.", + "Test command to run with Percy; infer from project context first.", ), integrationType: z .nativeEnum(PercyIntegrationTypeEnum) .describe( - "Specifies whether to integrate with Percy Web or Percy Automate. If not explicitly provided, prompt the user to select the desired integration type.", + "'web' or 'automate'. If unspecified, ask the user.", ), }; diff --git a/src/tools/testmanagement-utils/create-testcase.ts b/src/tools/testmanagement-utils/create-testcase.ts index eafff88a..a4cae558 100644 --- a/src/tools/testmanagement-utils/create-testcase.ts +++ b/src/tools/testmanagement-utils/create-testcase.ts @@ -98,16 +98,16 @@ export const CreateTestCaseSchema = z.object({ description: z .string() .optional() - .describe("Brief description of the test case."), + .describe("Brief description."), owner: z .string() .email() - .describe("Email of the test case owner.") + .describe("Owner email.") .optional(), preconditions: z .string() .optional() - .describe("Any preconditions (HTML allowed)."), + .describe("Preconditions (HTML allowed)."), test_case_steps: z .array( z.object({ @@ -115,19 +115,19 @@ export const CreateTestCaseSchema = z.object({ result: z.string().describe("Expected result of this step."), }), ) - .describe("List of steps and expected results."), + .describe("Steps and expected results."), issues: z .array(z.string()) .optional() .describe( - "List of the linked Jira, Asana or Azure issues ID's. This should be strictly in array format not the string of json.", + "Linked Jira/Asana/Azure issue IDs, as an array (not a JSON string).", ), issue_tracker: z .object({ name: z .string() .describe( - "Issue tracker name, For example, use jira for Jira, azure for Azure DevOps, or asana for Asana.", + "'jira', 'azure' or 'asana'.", ), host: z.string().url().describe("Base URL of the issue tracker."), }) @@ -136,43 +136,43 @@ export const CreateTestCaseSchema = z.object({ .array(z.string()) .optional() .describe( - "Tags to attach to the test case. This should be strictly in array format not the string of json", + "Tags, as an array (not a JSON string).", ), custom_fields: z .record(z.string(), customFieldValueSchema) .optional() .describe( - "Map of custom field NAME to value; use an array for multi-select fields.", + "Custom field NAME to value; array for multi-select.", ), automation_status: z .string() .optional() .describe( - "Automation status of the test case. Common values include 'not_automated', 'automated', 'automation_not_required'.", + "e.g. 'not_automated', 'automated', 'automation_not_required'.", ), priority: z .string() .optional() .describe( - "Priority, e.g. 'High' or 'medium'; values vary per project; default Medium.", + "e.g. 'High', 'medium'; varies per project; default Medium.", ), case_type: z .string() .optional() .describe( - "Test case type display or internal name (per-project). Omit for project default.", + "Test case type (varies per project); omit for default.", ), template: z .string() .optional() .describe( - "System template slug only: 'test_case_steps' or 'test_case_bdd'. For a custom template, use template_id instead.", + "'test_case_steps' or 'test_case_bdd'; for a custom template use template_id.", ), template_id: z .number() .optional() .describe( - "Numeric ID of a custom template (from listTestCaseTemplates); applies that template. Overrides 'template'.", + "Custom template ID (from listTestCaseTemplates); overrides 'template'.", ), }); diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts index 05383993..e0f37c4c 100644 --- a/src/tools/testmanagement-utils/update-testcase.ts +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -40,36 +40,36 @@ export const UpdateTestCaseSchema = z.object({ project_identifier: z .string() .describe( - "The ID of the BrowserStack project containing the test case to update.", + "Project ID of the test case.", ), test_case_identifier: z .string() .describe( - "The ID of the test case to update. This can be found using the listTestCases tool.", + "Test case ID (from listTestCases).", ), name: z.string().optional().describe("Updated name of the test case."), description: z .string() .optional() - .describe("Updated brief description of the test case."), + .describe("Updated description."), preconditions: z .string() .optional() - .describe("Updated preconditions for the test case."), + .describe("Updated preconditions."), test_case_steps: z .array( z.object({ - step: z.string().describe("The action to perform in this step."), - result: z.string().describe("The expected result of this step."), + step: z.string().describe("Step action."), + result: z.string().describe("Expected result."), }), ) .optional() - .describe("Updated list of test case steps with expected results."), + .describe("Updated steps and expected results."), owner: z .string() .email() .optional() - .describe("Email of the test case owner."), + .describe("Owner email."), priority: z .string() .optional() @@ -90,17 +90,17 @@ export const UpdateTestCaseSchema = z.object({ .string() .optional() .describe( - "Updated review status of the test case (e.g. 'active', 'draft', 'in_review', 'outdated', 'rejected').", + "e.g. 'active', 'draft', 'in_review', 'outdated', 'rejected'.", ), tags: z .array(z.string()) .optional() - .describe("Replacement list of tags for the test case."), + .describe("Replacement tags."), issues: z .array(z.string()) .optional() .describe( - "Replacement list of linked Jira/Asana/Azure issue IDs for the test case.", + "Replacement Jira/Asana/Azure issue IDs.", ), custom_fields: z .record( @@ -114,7 +114,7 @@ export const UpdateTestCaseSchema = z.object({ ) .optional() .describe( - "Map of custom field NAME to value; use an array for multi-select fields.", + "Custom field NAME to value; array for multi-select.", ), }); From 35ab1a7481590fc03364fe060248eeb234cb6184 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Fri, 30 Oct 2026 15:34:16 +0530 Subject: [PATCH 3/7] style: apply prettier to shortened parameter descriptions Co-Authored-By: Claude Opus 5.5 --- src/tools/sdk-utils/common/schema.ts | 4 +- .../testmanagement-utils/create-testcase.ts | 37 ++++------------ .../testmanagement-utils/update-testcase.ts | 43 ++++--------------- 3 files changed, 18 insertions(+), 66 deletions(-) diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 9e2f0c17..ffcd5592 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -120,9 +120,7 @@ export const RunPercyScanParamsShape = { ), integrationType: z .nativeEnum(PercyIntegrationTypeEnum) - .describe( - "'web' or 'automate'. If unspecified, ask the user.", - ), + .describe("'web' or 'automate'. If unspecified, ask the user."), }; export const FetchPercyChangesParamsShape = { diff --git a/src/tools/testmanagement-utils/create-testcase.ts b/src/tools/testmanagement-utils/create-testcase.ts index a4cae558..f6eaf530 100644 --- a/src/tools/testmanagement-utils/create-testcase.ts +++ b/src/tools/testmanagement-utils/create-testcase.ts @@ -95,15 +95,8 @@ export const CreateTestCaseSchema = z.object({ .string() .describe("Folder id. If none, offer createProjectOrFolder."), name: z.string().describe("Name of the test case."), - description: z - .string() - .optional() - .describe("Brief description."), - owner: z - .string() - .email() - .describe("Owner email.") - .optional(), + description: z.string().optional().describe("Brief description."), + owner: z.string().email().describe("Owner email.").optional(), preconditions: z .string() .optional() @@ -124,44 +117,30 @@ export const CreateTestCaseSchema = z.object({ ), issue_tracker: z .object({ - name: z - .string() - .describe( - "'jira', 'azure' or 'asana'.", - ), + name: z.string().describe("'jira', 'azure' or 'asana'."), host: z.string().url().describe("Base URL of the issue tracker."), }) .optional(), tags: z .array(z.string()) .optional() - .describe( - "Tags, as an array (not a JSON string).", - ), + .describe("Tags, as an array (not a JSON string)."), custom_fields: z .record(z.string(), customFieldValueSchema) .optional() - .describe( - "Custom field NAME to value; array for multi-select.", - ), + .describe("Custom field NAME to value; array for multi-select."), automation_status: z .string() .optional() - .describe( - "e.g. 'not_automated', 'automated', 'automation_not_required'.", - ), + .describe("e.g. 'not_automated', 'automated', 'automation_not_required'."), priority: z .string() .optional() - .describe( - "e.g. 'High', 'medium'; varies per project; default Medium.", - ), + .describe("e.g. 'High', 'medium'; varies per project; default Medium."), case_type: z .string() .optional() - .describe( - "Test case type (varies per project); omit for default.", - ), + .describe("Test case type (varies per project); omit for default."), template: z .string() .optional() diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts index 28bcbeb6..1d10918e 100644 --- a/src/tools/testmanagement-utils/update-testcase.ts +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -37,25 +37,13 @@ export interface TestCaseUpdateRequest { } export const UpdateTestCaseSchema = z.object({ - project_identifier: z - .string() - .describe( - "Project ID of the test case.", - ), + project_identifier: z.string().describe("Project ID of the test case."), test_case_identifier: z .string() - .describe( - "Test case ID (from listTestCases).", - ), + .describe("Test case ID (from listTestCases)."), name: z.string().optional().describe("Updated name of the test case."), - description: z - .string() - .optional() - .describe("Updated description."), - preconditions: z - .string() - .optional() - .describe("Updated preconditions."), + description: z.string().optional().describe("Updated description."), + preconditions: z.string().optional().describe("Updated preconditions."), test_case_steps: z .array( z.object({ @@ -65,11 +53,7 @@ export const UpdateTestCaseSchema = z.object({ ) .optional() .describe("Updated steps and expected results."), - owner: z - .string() - .email() - .optional() - .describe("Owner email."), + owner: z.string().email().optional().describe("Owner email."), priority: z .string() .optional() @@ -89,19 +73,12 @@ export const UpdateTestCaseSchema = z.object({ status: z .string() .optional() - .describe( - "e.g. 'active', 'draft', 'in_review', 'outdated', 'rejected'.", - ), - tags: z - .array(z.string()) - .optional() - .describe("Replacement tags."), + .describe("e.g. 'active', 'draft', 'in_review', 'outdated', 'rejected'."), + tags: z.array(z.string()).optional().describe("Replacement tags."), issues: z .array(z.string()) .optional() - .describe( - "Replacement Jira/Asana/Azure issue IDs.", - ), + .describe("Replacement Jira/Asana/Azure issue IDs."), custom_fields: z .record( z.string(), @@ -113,9 +90,7 @@ export const UpdateTestCaseSchema = z.object({ ]), ) .optional() - .describe( - "Custom field NAME to value; array for multi-select.", - ), + .describe("Custom field NAME to value; array for multi-select."), }); /** From 6e52f9602aee433687c9bfa04c07690cbdab02b6 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Fri, 30 Oct 2026 15:38:14 +0530 Subject: [PATCH 4/7] fix(server): trim parameter text that main added since the branch After merging main, the hosted tool set measured ~30,070 chars, just over Kiro's 30,000 activation cap, mostly from the new accessibility report sources and invokeCapability's `select`. Shortens those parameter descriptions (fetchAccessibilityIssues, startAccessibilityScan, invokeCapability, setupBrowserStackAppAutomateTests, askBrowserStackAI) while keeping every source pointer and default. Hosted set: ~29,340 chars. Local 52-tool set: ~32,760. Co-Authored-By: Claude Opus 5.5 --- src/tools/accessibility.ts | 26 ++++++++----------- .../appautomate-utils/appium-sdk/constants.ts | 16 +++--------- src/tools/ask-browserstack/register.ts | 3 +-- src/tools/capability-registry/register.ts | 6 ++--- 4 files changed, 18 insertions(+), 33 deletions(-) diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index 326e91bf..2936bc04 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -1624,7 +1624,7 @@ export default function addAccessibilityTools( .enum(Object.keys(WCAG_VERSIONS) as [keyof typeof WCAG_VERSIONS]) .optional() .describe( - 'WCAG version and level to check against, e.g. "wcag22aa", or "rgaa41" for RGAA 4.1 (default "wcag21aa").', + 'WCAG level, e.g. "wcag22aa" or "rgaa41" (default "wcag21aa").', ), bestPractices: z .boolean() @@ -1772,26 +1772,26 @@ export default function addAccessibilityTools( .string() .optional() .describe( - "Website Scanner scan id: from startAccessibilityScan or list_website_scanner_scans (searchCapability).", + "Website Scanner scan id (startAccessibilityScan or searchCapability).", ), scanRunId: z .string() .optional() .describe( - "Website Scanner run id: from startAccessibilityScan, or list_website_scanner_scan_runs (searchCapability).", + "Website Scanner run id (startAccessibilityScan or searchCapability).", ), buildId: z .string() .regex(/^[a-z0-9]{40}$/, "40 lowercase letters and digits") .optional() .describe( - "Web or App Accessibility build id: the SDK's testhub id, a link's thBuildId, or one listed via searchCapability.", + "Web/App Accessibility build id: SDK testhub id or a link's thBuildId.", ), workflowReportId: z .string() .optional() .describe( - "Workflow Analyzer report id: from its link, or list_workflow_analyzer_reports (searchCapability).", + "Workflow Analyzer report id, from its link or searchCapability.", ), appReportId: z .string() @@ -1801,38 +1801,34 @@ export default function addAccessibilityTools( ) .optional() .describe( - "Manual App Accessibility report id (a UUID), from its /report/ link or a report listed via searchCapability.", + "Manual App Accessibility report UUID, from its /report/ link.", ), reportUrl: z .string() .optional() .describe( - "A web or app report dashboard link (scan, build, manual report), a scanner share link, or an (App) Automate link.", + "Any web/app accessibility report, scanner share, or Automate link.", ), cursor: z .number() .optional() .describe( - "Character offset for pagination (default: 0); with compareWith, how many issues are already listed.", + "Pagination offset (default 0); with compareWith, issues already listed.", ), pageUrl: z .string() .optional() - .describe( - "Only the issues on this page URL, counted as the dashboard's page view counts them.", - ), + .describe("Only the issues on this page URL."), compareWith: z .string() .optional() .describe( - '"previous", or an older scanRunId of the same scan, or an earlier app build\'s id or link; lists new and resolved issues.', + '"previous", an older scanRunId, or an earlier app build id/link; lists new and resolved issues.', ), exportTaskId: z .string() .optional() - .describe( - "Continue a CSV export that an earlier call said is still being generated.", - ), + .describe("Continue a CSV export that is still being generated."), }, { title: "Fetch Accessibility Issues", diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index 9df5565c..ed6a3c91 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -52,9 +52,7 @@ export const SETUP_APP_AUTOMATE_DESCRIPTION = export const SETUP_APP_AUTOMATE_SCHEMA = { detectedFramework: z .nativeEnum(AppSDKSupportedFrameworkEnum) - .describe( - "The mobile automation framework configured in the project. Example: 'appium'", - ), + .describe("Mobile automation framework, e.g. 'appium'."), detectedTestingFramework: z .nativeEnum(AppSDKSupportedTestingFrameworkEnum) @@ -64,9 +62,7 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { detectedLanguage: z .nativeEnum(AppSDKSupportedLanguageEnum) - .describe( - "The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'", - ), + .describe("Project language, e.g. 'java', 'csharp'."), devices: z .array(MobileDeviceSchema) @@ -78,9 +74,7 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { appPath: z .string() - .describe( - "Path to the mobile app file (.apk for Android, .ipa for iOS). Can be a local file path or a BrowserStack app URL (bs://)", - ), + .describe("Local .apk/.ipa path or a BrowserStack app URL (bs://)."), project: z .string() .optional() @@ -90,7 +84,5 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { .boolean() .optional() .default(false) - .describe( - "Run App Accessibility (WCAG) checks on the screens the tests touch; Android 11+ or iOS 14+.", - ), + .describe("Run App Accessibility (WCAG) checks; Android 11+ or iOS 14+."), }; diff --git a/src/tools/ask-browserstack/register.ts b/src/tools/ask-browserstack/register.ts index d26fc719..3bd10773 100644 --- a/src/tools/ask-browserstack/register.ts +++ b/src/tools/ask-browserstack/register.ts @@ -544,8 +544,7 @@ export function addAskBrowserStackAITool( product: z .enum(PRODUCTS) .describe( - "Which product to work in: tm (Test Management), a11y (Accessibility), " + - "tra (Test Reporting & Analytics).", + "Product: tm (Test Management), a11y (Accessibility), tra (Test Reporting & Analytics).", ), // OPTIONAL, because a resume does not carry one. `next_step` tells the model to // call again "with exactly these params" — `product`, `run_id`, `perm_id`, diff --git a/src/tools/capability-registry/register.ts b/src/tools/capability-registry/register.ts index c81cfd6a..7de210a7 100644 --- a/src/tools/capability-registry/register.ts +++ b/src/tools/capability-registry/register.ts @@ -1048,8 +1048,7 @@ export function addCapabilityRegistryTools( .string() .optional() .describe( - "The capability's published name, exactly as returned (e.g. 'create_test_run_by_integer_id'). " + - "Preferred over method/path.", + "Capability name exactly as returned. Preferred over method/path.", ), method: z .string() @@ -1112,8 +1111,7 @@ export function addCapabilityRegistryTools( }) .optional() .describe( - "Only for a capability whose contract carries `narrowable`: narrows the response " + - "to those fields and reports what it narrowed.", + "Fields to keep; only when the contract carries `narrowable`.", ), }, { From 163b51b8c02cfbbfbb8d84126783f60e2d675901 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Fri, 30 Oct 2026 16:34:39 +0530 Subject: [PATCH 5/7] fix(live): normalise version keywords in the schema, not the description The trimmed descriptions dropped the prose telling the model to turn "newest"/"most recent" into "latest" and "earliest"/"first" into "oldest", and the code only matches those two exact strings. Move the mapping into the schema with a shared versionString() (z.string() .transform), as CLAUDE.md prefers, so it holds whatever the model sends. Applied to runBrowserLiveSession (desiredOSVersion, desiredBrowserVersion), runAppLiveSession and takeAppScreenshot (desiredPlatformVersion). Matching is case- and whitespace-insensitive; version numbers and names such as "Sequoia" pass through unchanged. The advertised JSON schema is unchanged (type string, same description). Verified over MCP: desiredOSVersion "newest" + desiredBrowserVersion "most recent" resolve to Windows 11 / Chrome 156.0. Co-Authored-By: Claude Opus 5.5 --- src/lib/version-keyword.ts | 23 +++++++++++++ src/tools/appautomate.ts | 9 +++-- src/tools/applive.ts | 7 ++-- src/tools/live.ts | 12 +++---- tests/lib/version-keyword.test.ts | 56 +++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 src/lib/version-keyword.ts create mode 100644 tests/lib/version-keyword.test.ts diff --git a/src/lib/version-keyword.ts b/src/lib/version-keyword.ts new file mode 100644 index 00000000..669d35a0 --- /dev/null +++ b/src/lib/version-keyword.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +/** + * Version fields (OS, browser, platform) accept a version number or one of two keywords, + * "latest" and "oldest", and the code downstream compares against those exact strings. + * Models and users say the same thing in other words — "newest", "most recent", + * "earliest", "first", "Latest" — so the mapping lives here, in the schema, rather than in + * description prose a model may or may not follow. + */ +const LATEST = new Set(["latest", "newest", "most recent", "most-recent"]); +const OLDEST = new Set(["oldest", "earliest", "first"]); + +export function normalizeVersionKeyword(value: string): string { + const key = value.trim().toLowerCase().replace(/\s+/g, " "); + if (LATEST.has(key)) return "latest"; + if (OLDEST.has(key)) return "oldest"; + return value.trim(); +} + +/** A version string whose keyword synonyms are normalised to "latest" / "oldest". */ +export function versionString() { + return z.string().transform(normalizeVersionKeyword); +} diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index c91428fe..79d41cf3 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -10,6 +10,7 @@ import { remote } from "webdriverio"; import { AppTestPlatform } from "./appautomate-utils/native-execution/types.js"; import { setupAppAutomateHandler } from "./appautomate-utils/appium-sdk/handler.js"; import { validateAppAutomateDevices } from "./sdk-utils/common/device-validator.js"; +import { versionString } from "../lib/version-keyword.js"; import { SETUP_APP_AUTOMATE_DESCRIPTION, @@ -385,11 +386,9 @@ export default function addAppAutomationTools( desiredPhone: z .string() .describe("Exact device name, e.g. 'iPhone 15 Pro Max'. Ask the user."), - desiredPlatformVersion: z - .string() - .describe( - "The platform version to run the app on. Use 'latest' or 'oldest' for dynamic resolution.", - ), + desiredPlatformVersion: versionString().describe( + "Version number, or 'latest' / 'oldest'.", + ), desiredPlatform: z .enum([Platform.ANDROID, Platform.IOS]) .describe("Platform to run the app on. Either 'android' or 'ios'."), diff --git a/src/tools/applive.ts b/src/tools/applive.ts index e4e7a077..ce47e17f 100644 --- a/src/tools/applive.ts +++ b/src/tools/applive.ts @@ -7,6 +7,7 @@ import logger from "../logger.js"; import { trackMCP } from "../lib/instrumentation.js"; import { BrowserStackConfig } from "../lib/types.js"; import { isEnabled as capabilityRegistryEnabled } from "./capability-registry/config.js"; +import { versionString } from "../lib/version-keyword.js"; /** * Launches an App Live Session on BrowserStack. @@ -101,9 +102,9 @@ export default function addAppLiveTools( .describe( "Exact device name, e.g. 'iPhone 15 Pro Max'. Ask the user; never assume.", ), - desiredPlatformVersion: z - .string() - .describe("Version number, or 'latest' / 'oldest'."), + desiredPlatformVersion: versionString().describe( + "Version number, or 'latest' / 'oldest'.", + ), desiredPlatform: z .enum(["android", "ios"]) .describe( diff --git a/src/tools/live.ts b/src/tools/live.ts index 3cb251af..be32aa74 100644 --- a/src/tools/live.ts +++ b/src/tools/live.ts @@ -7,6 +7,7 @@ import { trackMCP } from "../lib/instrumentation.js"; import { BrowserStackConfig } from "../lib/types.js"; import { isEnabled as capabilityRegistryEnabled } from "./capability-registry/config.js"; import globalConfig from "../config.js"; +import { versionString } from "../lib/version-keyword.js"; // Define the schema shape const LiveArgsShape = { @@ -19,16 +20,13 @@ const LiveArgsShape = { .describe( "Desktop OS ('Windows' or 'OS X') or mobile OS ('android','ios','winphone')", ), - desiredOSVersion: z - .string() - .describe( - "Version number or 'latest'/'oldest'; macOS takes names, e.g. 'Sequoia'.", - ), + desiredOSVersion: versionString().describe( + "Version number or 'latest'/'oldest'; macOS takes names, e.g. 'Sequoia'.", + ), desiredBrowser: z .enum(["chrome", "firefox", "safari", "edge", "ie"]) .describe("Browser for desktop (Chrome, IE, Firefox, Safari, Edge)"), - desiredBrowserVersion: z - .string() + desiredBrowserVersion: versionString() .optional() .describe("Desktop browser version, or 'latest' / 'oldest'."), desiredDevice: z.string().optional().describe("Device name for mobile"), diff --git a/tests/lib/version-keyword.test.ts b/tests/lib/version-keyword.test.ts new file mode 100644 index 00000000..a9d7b028 --- /dev/null +++ b/tests/lib/version-keyword.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { z } from "zod"; +import { + normalizeVersionKeyword, + versionString, +} from "../../src/lib/version-keyword"; + +// The Live, App Live and takeAppScreenshot version fields used to tell the model, in prose, +// to turn "newest" into "latest" and "earliest" into "oldest". The trimmed descriptions no +// longer say so, and the code downstream only matches the exact strings, so the schema +// does the mapping. +describe("normalizeVersionKeyword", () => { + it.each([ + ["latest", "latest"], + ["Latest", "latest"], + ["newest", "latest"], + ["most recent", "latest"], + [" Most Recent ", "latest"], + ["most-recent", "latest"], + ["oldest", "oldest"], + ["earliest", "oldest"], + ["First", "oldest"], + ])("maps %j to %j", (input, expected) => { + expect(normalizeVersionKeyword(input)).toBe(expected); + }); + + it.each(["14", "14.0", "133.2", "Sequoia", "11"])( + "passes the version %j through unchanged", + (input) => { + expect(normalizeVersionKeyword(input)).toBe(input); + }, + ); + + it("trims surrounding whitespace from a version number", () => { + expect(normalizeVersionKeyword(" 17 ")).toBe("17"); + }); +}); + +describe("versionString schema", () => { + it("normalises when parsed as part of a tool's argument object", () => { + const shape = z.object({ + desiredOSVersion: versionString(), + desiredBrowserVersion: versionString().optional(), + }); + expect( + shape.parse({ desiredOSVersion: "newest", desiredBrowserVersion: "earliest" }), + ).toEqual({ desiredOSVersion: "latest", desiredBrowserVersion: "oldest" }); + expect(shape.parse({ desiredOSVersion: "Sequoia" })).toEqual({ + desiredOSVersion: "Sequoia", + }); + }); + + it("still rejects a non-string", () => { + expect(() => versionString().parse(14)).toThrow(); + }); +}); From 7ed8889b90e588ecf16b388fadd5a64412667332 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Fri, 30 Oct 2026 17:47:11 +0530 Subject: [PATCH 6/7] fix(server): restore dropped routing guidance and guard the description budget - searchCapability: paging stops when next_offset is absent - invokeCapability: page with the capability's own parameter, not offset - tool handoff: no tool lists projects, so use the capability functions - askBrowserStackAI: read-only runs return changes in needs_approval - setupPercyVisualTesting: only for projects that already have Percy - updateTestCase: list cannot_be_automated again - budget test: lower grandfathered ceilings to the trimmed lengths and cap total description text at 21,500 characters Co-Authored-By: Claude Opus 5.5 --- src/tools/ask-browserstack/register.ts | 3 +- src/tools/capability-registry/register.ts | 4 +- src/tools/sdk-utils/common/constants.ts | 2 +- .../testmanagement-utils/update-testcase.ts | 2 +- src/tools/tool-handoff.ts | 4 +- tests/lib/version-keyword.test.ts | 5 +- tests/tools/toolDescriptionBudget.test.ts | 79 +++++++++++-------- 7 files changed, 57 insertions(+), 42 deletions(-) diff --git a/src/tools/ask-browserstack/register.ts b/src/tools/ask-browserstack/register.ts index 3bd10773..b30984fb 100644 --- a/src/tools/ask-browserstack/register.ts +++ b/src/tools/ask-browserstack/register.ts @@ -138,7 +138,8 @@ const DESCRIPTION = "here fits, or when the ones you tried did not get you there. Prefer a specific tool " + "whenever one fits. Describe the task in plain language. A data change asks you to " + "confirm first: put it to the user, never decide for them; deletes are refused " + - "outright. One task per call."; + "outright. If no one can confirm, the run is read-only and changes come back in " + + "`needs_approval`. One task per call."; /** * `isError` marks a call that FAILED, not one that was refused. diff --git a/src/tools/capability-registry/register.ts b/src/tools/capability-registry/register.ts index 7de210a7..fd9a76e2 100644 --- a/src/tools/capability-registry/register.ts +++ b/src/tools/capability-registry/register.ts @@ -522,7 +522,7 @@ export function addCapabilityRegistryTools( tools.searchCapability = server.tool( "searchCapability", - "Find capabilities in ONE product by plain language. CALL listProducts FIRST; `product` must be spelled exactly as listProducts spells it. If `weak_match` is true, search again with `suggested_vocabulary` before invoking. Results are a shortlist: call describeCapability for the contract, then invokeCapability. query='*' lists all; page with `next_offset`.", + "Find capabilities in ONE product by plain language. CALL listProducts FIRST; `product` must be spelled exactly as listProducts spells it. If `weak_match` is true, search again with `suggested_vocabulary` before invoking. Results are a shortlist: call describeCapability for the contract, then invokeCapability. query='*' lists all; page with `next_offset` until it is absent.", { query: z .string() @@ -1042,7 +1042,7 @@ export function addCapabilityRegistryTools( tools.invokeCapability = server.tool( "invokeCapability", - "Run a capability after describeCapability, by `name` (or `method` + `path` if unnamed); args go in path_params / query / body. `select` narrows the response only where the contract carries `narrowable`. If `completed` is false, fetch the next page. Writes: ask the user first, then resend with user_permission='granted' and a change_summary. Deletes are refused; archive, close and merge are writes.", + "Run a capability after describeCapability, by `name` (or `method` + `path` if unnamed); args go in path_params / query / body. `select` narrows the response only where the contract carries `narrowable`. If `completed` is false, fetch the next page with the capability's own page parameter, not `offset`. Writes: ask the user first, then resend with user_permission='granted' and a change_summary. Deletes are refused; archive, close and merge are writes.", { name: z .string() diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts index 96e2b7c5..2f8294cb 100644 --- a/src/tools/sdk-utils/common/constants.ts +++ b/src/tools/sdk-utils/common/constants.ts @@ -3,7 +3,7 @@ export const IMPORTANT_SETUP_WARNING = "If you cannot run commands or edit files in the user's project (e.g. a chat-only client), present every step below to the user in full — including each shell command, the package.json changes, and the complete browserstack.yml contents — instead of summarizing them."; export const SETUP_PERCY_DESCRIPTION = - "Set up or expand Percy visual testing coverage in a project, with or without existing Percy (Percy Web and Percy Automate)."; + "Expand Percy visual testing coverage in a project that already has Percy (Percy Web and Percy Automate)."; export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories."; diff --git a/src/tools/testmanagement-utils/update-testcase.ts b/src/tools/testmanagement-utils/update-testcase.ts index 1d10918e..076596f6 100644 --- a/src/tools/testmanagement-utils/update-testcase.ts +++ b/src/tools/testmanagement-utils/update-testcase.ts @@ -68,7 +68,7 @@ export const UpdateTestCaseSchema = z.object({ .string() .optional() .describe( - "e.g. not_automated, automated, automation_not_required, obsolete.", + "e.g. not_automated, automated, automation_not_required, cannot_be_automated, obsolete.", ), status: z .string() diff --git a/src/tools/tool-handoff.ts b/src/tools/tool-handoff.ts index 00ef316e..36052d9c 100644 --- a/src/tools/tool-handoff.ts +++ b/src/tools/tool-handoff.ts @@ -25,7 +25,7 @@ /** No tool lists projects, so this genuinely has to go to the agent. */ export const NEEDS_PROJECT_ID = - " Needs a project id (PR-*); if you lack one, get it from the capability functions."; + " Needs a project id (PR-*); no tool lists projects, so get it from the capability functions."; /** A sibling tool can produce the id — prefer it over the agent. */ export function needsIdFrom(idLabel: string, sourceTool: string): string { @@ -40,7 +40,7 @@ export function needsIdFrom(idLabel: string, sourceTool: string): string { */ export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier; a folder inside an EXISTING project needs " + - "its id (PR-*), from the capability functions."; + "its id (PR-*); no tool lists projects, so use the capability functions."; /** A test plan id (TP-*) comes from listTestPlans. */ export const NEEDS_TEST_PLAN_ID = needsIdFrom( diff --git a/tests/lib/version-keyword.test.ts b/tests/lib/version-keyword.test.ts index a9d7b028..e9fb92c4 100644 --- a/tests/lib/version-keyword.test.ts +++ b/tests/lib/version-keyword.test.ts @@ -43,7 +43,10 @@ describe("versionString schema", () => { desiredBrowserVersion: versionString().optional(), }); expect( - shape.parse({ desiredOSVersion: "newest", desiredBrowserVersion: "earliest" }), + shape.parse({ + desiredOSVersion: "newest", + desiredBrowserVersion: "earliest", + }), ).toEqual({ desiredOSVersion: "latest", desiredBrowserVersion: "oldest" }); expect(shape.parse({ desiredOSVersion: "Sequoia" })).toEqual({ desiredOSVersion: "Sequoia", diff --git a/tests/tools/toolDescriptionBudget.test.ts b/tests/tools/toolDescriptionBudget.test.ts index bf87bbb7..b6fc1178 100644 --- a/tests/tools/toolDescriptionBudget.test.ts +++ b/tests/tools/toolDescriptionBudget.test.ts @@ -14,42 +14,30 @@ vi.mock("../../src/logger.js", () => ({ const CEILING = 200; -/** The tools already over the ceiling, at their length on main (2.2.6). Lower only. */ +/** The tools already over the ceiling, at their trimmed length. Lower only. */ const GRANDFATHERED: Record = { - addTestResult: 293, - askBrowserStackAI: 1006, - createLCASteps: 340, - createProjectOrFolder: 264, - createTestCase: 283, - createTestRun: 265, - describeCapability: 753, - describeEntity: 219, - expandPercyVisualTesting: 266, - fetchRCA: 236, - fetchSelfHealedSelectors: 452, - getFailureLogs: 242, - getSubTestPlan: 649, - getTestPlan: 807, - invokeCapability: 852, - listFolders: 437, - listProducts: 856, - listSubTestPlans: 655, - listTestCases: 472, - listTestPlans: 652, - listTestRuns: 301, - percyVisualTestIntegrationAgent: 342, - prepareSelfHealingPlan: 806, - runAppTestsOnBrowserStack: 297, - searchCapability: 2019, - setupBrowserStackAppAutomateTests: 268, - setupBrowserStackAutomateTests: 328, - updateTestCase: 501, - updateTestRun: 272, - uploadProductRequirementFile: 429, + askBrowserStackAI: 532, + createProjectOrFolder: 217, + describeCapability: 201, + fetchSelfHealedSelectors: 226, + getSubTestPlan: 394, + getTestPlan: 422, + invokeCapability: 454, + listProducts: 252, + listSubTestPlans: 396, + listTestPlans: 325, + prepareSelfHealingPlan: 261, + searchCapability: 375, + uploadProductRequirementFile: 306, }; -/** Each tool's description as a client lists it at connect time. */ -async function descriptions(): Promise<[string, number][]> { +// Tool and parameter descriptions across every tool, in characters. Kiro renders the hosted +// subset of this text into one activation result capped at 30,000; at this total it is about +// 29,600. Lower only. +const TOTAL_CEILING = 21_500; + +/** Each tool as a client lists it at connect time. */ +async function listedTools() { const { BrowserStackMcpServer } = await import("../../src/server-factory.js"); const server = new BrowserStackMcpServer({ "browserstack-username": "u", @@ -63,7 +51,15 @@ async function descriptions(): Promise<[string, number][]> { ]); const { tools } = await client.listTools(); await client.close(); - return tools.map((t) => [t.name, (t.description ?? "").length]); + return tools; +} + +/** Each tool's description length. */ +async function descriptions(): Promise<[string, number][]> { + return (await listedTools()).map((t) => [ + t.name, + (t.description ?? "").length, + ]); } const budget = (name: string) => Math.max(CEILING, GRANDFATHERED[name] ?? 0); @@ -79,4 +75,19 @@ describe("tool descriptions", () => { .map(([name, length]) => `${name}: ${length} > ${budget(name)}`); expect(over).toEqual([]); }); + + it("keeps the total description text within its ceiling", async () => { + const total = (await listedTools()).reduce((sum, t) => { + const props = (t.inputSchema.properties ?? {}) as Record< + string, + { description?: string } + >; + const params = Object.values(props).reduce( + (s, p) => s + (p.description ?? "").length, + 0, + ); + return sum + (t.description ?? "").length + params; + }, 0); + expect(total).toBeLessThanOrEqual(TOTAL_CEILING); + }); }); From 36dbe8ac416c74302319994624e0c778f22da7c7 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Fri, 30 Oct 2026 17:47:23 +0530 Subject: [PATCH 7/7] fix(percy): word expandPercyVisualTesting for existing projects Co-Authored-By: Claude Opus 5.5 --- src/tools/sdk-utils/common/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts index 2f8294cb..af3e9226 100644 --- a/src/tools/sdk-utils/common/constants.ts +++ b/src/tools/sdk-utils/common/constants.ts @@ -3,7 +3,7 @@ export const IMPORTANT_SETUP_WARNING = "If you cannot run commands or edit files in the user's project (e.g. a chat-only client), present every step below to the user in full — including each shell command, the package.json changes, and the complete browserstack.yml contents — instead of summarizing them."; export const SETUP_PERCY_DESCRIPTION = - "Expand Percy visual testing coverage in a project that already has Percy (Percy Web and Percy Automate)."; + "Set up or expand Percy visual testing coverage in an existing project that may already use Percy (Percy Web and Percy Automate)."; export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories.";