Before you start
What problem does this solve?
registerAssistantTool (GeoLibreAppAPI) lets a plugin expose a tool to the in-app
AI Assistant, but it requires passing a real Tool instance built with
@strands-agents/sdk's tool() + a zod inputSchema. Built-in plugins
(packages/plugins/src/plugins/*) can do this because that package already
depends on the SDK and zod directly.
External plugins (loaded from a manifest/zip URL, see docs/plugin-api.md)
are standalone ES modules built independently of the app, and deliberately
avoid bundling the full agent SDK — it carries a partly server-oriented
surface (e.g. an MCP client, tracing) that isn't relevant in a browser
context and would bloat what's meant to be a lightweight drop-in. Because of
that, an external plugin today has no way at all to contribute an
assistant tool, even when it already has a working "fetch data + mutate the
map" action wired to a UI button.
Concretely: a plugin panel with an "Add to X" button that already fetches
data and calls app.addGeoJsonLayer(...) cannot let the AI Assistant trigger
that same action — the user must always click through the panel manually,
even though the plugin already contains everything needed to do it itself.
Proposed solution
Add a second, JSON-Schema-based registration entry point to GeoLibreAppAPI,
alongside the existing registerAssistantTool:
type AssistantToolSpec = {
name: string;
description: string;
inputSchema?: JSONSchema; // plain JSON Schema, not a zod schema
callback: (input: unknown) => unknown | Promise<unknown>;
};
registerAssistantToolSpec?: (
spec: AssistantToolSpec,
ownerPluginId?: string,
) => () => void;
The host builds a real Tool from the spec using the SDK's existing
JSON-Schema tool() overload (FunctionToolConfig, which already accepts a
plain JSON Schema instead of a zod schema — no SDK change needed) and
forwards it to the existing registerAssistantTool. Both paths land in the
same single registry (assistant-tool-registry.ts); nothing changes in how
apps/geolibre-desktop/src/lib/assistant/tools.ts consumes tools
(listAssistantTools() keeps working as-is).
One behavior to document clearly: unlike a zod-backed Tool, a JSON-Schema
spec's input is not validated automatically before the callback runs
(FunctionTool doesn't parse) — the callback must validate its own input.
This should be called out in the JSDoc for AssistantToolSpec.
Also worth documenting registerAssistantToolSpec in docs/plugin-api.md,
since registerAssistantTool itself currently has no mention there either.
I'm happy to open a PR with this change if the approach sounds right.
Estimated scope
Small: a minor tweak, small UI change, or text adjustment
Alternatives considered
-
Requiring external plugins to bundle @strands-agents/sdk + zod themselves
and call registerAssistantTool directly. Rejected: pulls a large, partly
server-oriented SDK surface into what's meant to be a lightweight drop-in
bundle, and hand-constructing an SDK Tool duck-types undocumented internals
(e.g. ToolResultBlock) with no test coverage for third-party bundles.
-
A second, independent tool registry just for external plugins. Rejected:
would let the same user-facing action be reachable through two disconnected
code paths with no shared state — exactly the inconsistency
registerAssistantTool already avoids for built-in plugins (one function
backs both a panel's button and the assistant tool).
Before you start
What problem does this solve?
registerAssistantTool(GeoLibreAppAPI) lets a plugin expose a tool to the in-appAI Assistant, but it requires passing a real
Toolinstance built with@strands-agents/sdk'stool()+ a zodinputSchema. Built-in plugins(
packages/plugins/src/plugins/*) can do this because that package alreadydepends on the SDK and zod directly.
External plugins (loaded from a manifest/zip URL, see docs/plugin-api.md)
are standalone ES modules built independently of the app, and deliberately
avoid bundling the full agent SDK — it carries a partly server-oriented
surface (e.g. an MCP client, tracing) that isn't relevant in a browser
context and would bloat what's meant to be a lightweight drop-in. Because of
that, an external plugin today has no way at all to contribute an
assistant tool, even when it already has a working "fetch data + mutate the
map" action wired to a UI button.
Concretely: a plugin panel with an "Add to X" button that already fetches
data and calls
app.addGeoJsonLayer(...)cannot let the AI Assistant triggerthat same action — the user must always click through the panel manually,
even though the plugin already contains everything needed to do it itself.
Proposed solution
Add a second, JSON-Schema-based registration entry point to
GeoLibreAppAPI,alongside the existing
registerAssistantTool:The host builds a real
Toolfrom the spec using the SDK's existingJSON-Schema
tool()overload (FunctionToolConfig, which already accepts aplain JSON Schema instead of a zod schema — no SDK change needed) and
forwards it to the existing
registerAssistantTool. Both paths land in thesame single registry (
assistant-tool-registry.ts); nothing changes in howapps/geolibre-desktop/src/lib/assistant/tools.tsconsumes tools(
listAssistantTools()keeps working as-is).One behavior to document clearly: unlike a zod-backed
Tool, a JSON-Schemaspec's input is not validated automatically before the callback runs
(
FunctionTooldoesn't parse) — the callback must validate its own input.This should be called out in the JSDoc for
AssistantToolSpec.Also worth documenting
registerAssistantToolSpecin docs/plugin-api.md,since
registerAssistantToolitself currently has no mention there either.I'm happy to open a PR with this change if the approach sounds right.
Estimated scope
Small: a minor tweak, small UI change, or text adjustment
Alternatives considered
Requiring external plugins to bundle @strands-agents/sdk + zod themselves
and call registerAssistantTool directly. Rejected: pulls a large, partly
server-oriented SDK surface into what's meant to be a lightweight drop-in
bundle, and hand-constructing an SDK Tool duck-types undocumented internals
(e.g. ToolResultBlock) with no test coverage for third-party bundles.
A second, independent tool registry just for external plugins. Rejected:
would let the same user-facing action be reachable through two disconnected
code paths with no shared state — exactly the inconsistency
registerAssistantTool already avoids for built-in plugins (one function
backs both a panel's button and the assistant tool).