Client tools¶
Some systems authenticate in the browser, as the user: a Kubernetes cluster
behind a cloud identity, Azure DevOps or GitHub through the user's own OAuth
session, anything the frontend already calls with a per-user token. A
client tool lets an assistant use such a system with the user's own
permissions. A frontend plugin or module contributes the tool through
AssistantClientToolBlueprint (see Contribute a tool);
the browser declares it on every /chat request, the backend offers it to
the model with no execute,
and the browser runs it when the model calls it. The result returns as the
tool output; the credential never leaves the browser, and client tools are
never exposed over MCP.
Nothing is configured in the backend. Give an assistant a client tool by adding its
<namespace>__<tool> name to the assistant's tools in the editor, which
offers every tool registered in the app; an assistant offers the built-in
client tools plus only the contributed ones it allows, whatever the browser
declares. The backend also drops a
declaration whose name is not <namespace>__<tool>, collides with a server
tool, or exceeds 4000 description characters or 8 KB of schema, and offers at
most 30 per request, built-ins included. Client tool results are capped to toolResultMaxChars
like every other tool result.
A client tool that is not read-only, or is destructive, asks the user for
approval on every call, with no "always allow"; a read-only one asks when its
name is in requireApproval. A tool
that needs a credential the user has not granted shows a Connect control
in the conversation; dismissing it returns a not_authorized result the
model explains instead of retrying.
Contribute a tool¶
A client tool runs in the user's browser with the user's own credentials, so
an assistant can use a system the frontend already talks to as the user
(a Kubernetes cluster, Azure DevOps, GitHub, an internal API behind the
user's OAuth session). Any plugin or app module contributes one with
AssistantClientToolBlueprint; the assistants plugin sends its declaration
to the backend on every turn, runs its execute when the model calls it, and
returns the result to the model. The credential never leaves the browser.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | |
Add the module to the app's features, then give an assistant the tool by
name in the editor's Tools section, under "Browser tools". The contract:
nameis<namespace>__<tool>; the namespace keeps a plugin's tools apart from Backstage actions and MCP servers.description(at most 4000 characters) andinputSchema(a JSON Schema object, at most 8 KB) are what the model sees.attributes.readOnlyanddestructivemirror Backstage action attributes. A tool that is not read-only, or is destructive, asks the user for approval on every call; there is no "always allow". A read-only tool asks when the backend'srequireApprovalnames it.authis optional.oauthnames a Backstage OAuth API and scopes;customsupplies anacquire({ apis, interactive })for a plugin that manages its own credentials (for example Kubernetes' auth-provider API). The runtime acquires silently first; when that yields nothing it shows a Connect control and acquires on the click, so no popup is blocked. The credential reaches onlyexecute.execute(input, { apis, credential, signal })returns a JSON-serializable result.apisis the app's API holder, so a tool can call any frontend API (catalogApiRef, a plugin's own API) rather than fetch directly. A thrown error, a timeout (timeoutMs, default 30 s, at most 120 s), a declined approval, or a dismissed Connect each become an{ _error: true, code, message }result the model explains.renderis optional for a tool withexecute(the call otherwise shows its input and output) and required for a tool without one, whose card supplies the result withaddResult; that is howrender_formworks.isAvailable({ apis })hides a tool that cannot work in this app.
Keep results small and explicit: return the fields the model needs, never a raw object dump, and never a secret. Everything a tool returns is untrusted input to the model.
With a model whose provider runs its own tool loop (an agent SDK or CLI a
backend module adds; see
Model providers),
the turn can't pause for the browser, so it holds the call open instead: a
tool's execute runs at once and its result returns to the same turn, and a
card tool such as render_form returns its addResult. A tool that would
ask first (one that writes, or one that needs Connect) is declined on such a
model, and the model is told why. Approval cards for the backend's gated tools
work as usual.