Skip to content

feat(tools): declare preconditions, so a missing id becomes a handoff - #381

Merged
sarve-shreyas merged 1 commit into
askrelay/central-mcpfrom
feat/tool-preconditions
Aug 31, 2026
Merged

feat(tools): declare preconditions, so a missing id becomes a handoff#381
sarve-shreyas merged 1 commit into
askrelay/central-mcpfrom
feat/tool-preconditions

Conversation

@sarve-shreyas

Copy link
Copy Markdown
Collaborator

The problem

Nothing routes an MCP call. The client's model picks a tool from the descriptions alone.
So a tool that needs an identifier the caller doesn't have is a dead end: the model either
asks the user for something they also don't know, or gives up. It does not reach for a
fallback, because no description tells it one exists.

The case that prompted this — "List all projects in my workspace":

TM tools requiring project_identifier 15 of 17
…that accept its absence 0
Tools in this server that list projects 0

The first step of nearly every Test Management journey was unserved. The model had nothing
to prefer and no handoff to follow, so it only reached askBrowserstackAI when a user
named it explicitly — which is not something a user should have to know.

The change

One sentence appended per tool, naming where the missing id comes from. A sibling tool
whenever one can produce it
— faster and more predictable than an agent, exactly as
askBrowserstackAI's own description argues. askBrowserstackAI only where no tool can.

Product Tool(s) Points at
tm 15 tools project id → askBrowserstackAI (nothing lists projects)
getTestPlan, listSubTestPlans, getSubTestPlan also: test plan id → listTestPlans
a11y fetchAccessibilityIssues scan ids → startAccessibilityScan, else the agent
getAccessibilityAuthConfig id → createAccessibilityAuthConfig, else the agent
tra listTestIds build id → getBuildId / listBuildId
fetchRCA test ids → listTestIds
getFailureLogs, fetchSelfHealedSelectors, prepareSelfHealingPlan session id → nothing lists it, so the agent

Composed, listTestCases now reads:

List test cases in a project, optionally scoped to a specific folder. … Requires a
project identifier (PR-*). No tool here lists projects, so if you do not have one, call
askBrowserstackAI with product "tm" and ask which projects exist, then retry this tool
with the identifier it returns.

Held as shared constants in src/tools/tool-handoff.ts, not per-tool prose: the wording
is the routing signal, and twenty hand-written variants drift into twenty different
signals. Rewording later is one edit.

Deliberately not included

A listProjects tool. It would also fix this query and is arguably the better
primitive — but it's a separate decision about the tool surface, and this change is the
general fix: it helps every tool that needs an id the caller lacks, not just the project
case. Worth doing as well, not instead.

What this does not fix

Tool choice is a model decision, and it varies between runs — we've seen the same query
land on different tools across two passes. This raises the probability that a stuck model
finds the next step; it cannot guarantee it. Any claim of deterministic routing would need
a different architecture, not better descriptions.

Testing

  • Full suite green: 482 tests / 32 files, typecheck clean.
  • Descriptions are data, so the real check is that each sentence landed on the
    description argument and not on the schema. Verified programmatically: all 25
    insertions follow a string literal, none follows a bare identifier.

Two mechanical mistakes were caught and corrected on the way, both worth knowing if anyone
repeats this pass:

  1. The bare tool-name string also appears inside handlers (trackMCP, error paths), so
    anchoring on "toolName", rewrote live code. Insertion is anchored on server.tool(.
  2. In rca-agent.ts the schema argument is a bare identifier (FETCH_RCA_PARAMS) rather
    than XSchema.shape, so the first attempt appended the sentence to the schema. The
    resulting type error also revealed that fetchRCA takes testId[], not a build id — so
    it points at listTestIds, which was a genuine content fix, not just a syntax one.

Base

Branched off askrelay/central-mcp (PR #379), because the handoff text names
askBrowserstackAI and that tool does not exist on main. If #379 lands first this rebases
cleanly; if #379 is deferred, the askBrowserstackAI pointers need to become
listProjects or be dropped, and the sibling-tool pointers still stand on their own.

Nothing routes an MCP call: the client's model picks a tool from the descriptions
alone. A tool that needs an identifier the caller does not have is a dead end -
the model asks the user for something they also do not know, or gives up. It does
not reach for a fallback, because no description tells it one exists.

The concrete case that prompted this: "List all projects in my workspace".

  - 15 of the 17 Test Management tools require project_identifier
  - NONE of them accepts its absence
  - NO tool in this server lists projects

So the first step of nearly every Test Management journey was unserved, and the
model had nothing to prefer AND no handoff to follow. It only reached
askBrowserstackAI when a user named it explicitly.

This appends one sentence per tool naming where the missing id comes from. A
sibling tool whenever one can produce it - faster and more predictable than an
agent, exactly as askBrowserstackAI's own description argues. askBrowserstackAI
only where no tool here can:

  tm    15 tools -> project id via askBrowserstackAI (nothing lists projects)
        3 of those also -> test plan id via listTestPlans
  a11y  fetchAccessibilityIssues   -> scan ids from startAccessibilityScan
        getAccessibilityAuthConfig -> id from createAccessibilityAuthConfig
  tra   listTestIds    -> build id via getBuildId / listBuildId
        fetchRCA       -> test ids via listTestIds
        getFailureLogs, fetchSelfHealedSelectors, prepareSelfHealingPlan
                       -> session id, which nothing lists

Kept as shared constants in tool-handoff.ts rather than per-tool prose: the
wording IS the routing signal, and twenty hand-written variants drift into twenty
different signals.

Deliberately NOT included: a listProjects tool. That would also fix this query
and is arguably the better primitive, but it is a separate decision about the
tool surface; this change is the general fix and helps every tool that needs an
id, not just the project case.

Two things the mechanical pass got wrong and were corrected: the bare tool-name
string also appears inside handlers (trackMCP, error paths), so anchoring on it
rewrote live code - insertion is anchored on server.tool( instead; and in
rca-agent.ts the schema argument is a bare identifier rather than X.shape, so the
first attempt appended to the SCHEMA. Its type error also revealed that fetchRCA
takes testId[], not a build id, so it points at listTestIds.

Full suite green (482 tests), typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sarve-shreyas
sarve-shreyas merged commit 4dd5daa into askrelay/central-mcp Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant