Metadata-Version: 2.4
Name: adk-tool-search
Version: 0.3.0
Summary: Dynamic tool search for Google ADK — load tools on demand instead of all at once
Author: ADK Tool Search Contributors
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/manojlds/adk-tool-search
Project-URL: Documentation, https://github.com/manojlds/adk-tool-search
Project-URL: Repository, https://github.com/manojlds/adk-tool-search
Project-URL: Issues, https://github.com/manojlds/adk-tool-search/issues
Keywords: adk,agent,tools,ai,llm,gemini,mcp,tool-search
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: google-adk>=1.29.0
Requires-Dist: rank-bm25>=0.2.2
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: pytest-timeout>=2.0; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: litellm>=1.81; extra == "dev"
Requires-Dist: python-dotenv>=1.0; extra == "dev"

# adk-tool-search

ADK-native deferred tool discovery for large function and MCP catalogs.

`SearchableToolset` exposes four lightweight management tools initially. The model searches the
catalog, activates an exact result, and ADK exposes the selected tool on the next model step.
Selected tools execute through ADK's normal tool pipeline, preserving authentication,
confirmation, callbacks, plugins, tracing, and lifecycle behavior.

## Why

Large tool catalogs increase prompt size and make tool selection less reliable. Tool search keeps
full schemas out of the initial model request while retaining a searchable local catalog.

```text
Initial request: search_tools, load_tool, unload_tool, clear_loaded_tools
       ↓
search_tools("weather by city")
       ↓
[{"name": "get_weather", "description": "...", "score": 4.2}]
       ↓
load_tool("get_weather")
       ↓
Next model step: management tools + get_weather
```

## Install

```bash
pip install adk-tool-search
```

## Function Tools

```python
from google.adk.agents import LlmAgent

from adk_tool_search import SearchableToolset


def get_weather(location: str) -> dict:
    """Get current weather for a location.

    Args:
        location: City or coordinates.
    """
    return {"location": location, "temperature": 22}


toolset = SearchableToolset(
    namespace="assistant",
    tools=[get_weather],
    max_loaded_tools=20,
)

agent = LlmAgent(
    name="assistant",
    model="gemini-2.5-flash",
    instruction=(
        "Use search_tools to discover capabilities, load_tool to activate an exact result, "
        "then call the activated tool."
    ),
    tools=[toolset],
)
```

Loaded names are persisted in session state under:

```text
adk_tool_search.loaded_tools.<namespace>
```

## MCP Tools

Wrap the `McpToolset` instead of detaching its tools at startup:

```python
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool import McpToolset, StdioConnectionParams
from mcp import StdioServerParameters

from adk_tool_search import SearchableToolset


mcp = McpToolset(
    connection_params=StdioConnectionParams(
        server_params=StdioServerParameters(
            command="npx",
            args=["-y", "@modelcontextprotocol/server-github"],
        )
    )
)

github_tools = SearchableToolset(
    namespace="github",
    source=mcp,
    tool_name_prefix="github",
)

agent = LlmAgent(
    name="github_assistant",
    model="gemini-2.5-flash",
    tools=[github_tools],
)
```

The wrapper delegates source authentication and `close()`, so ADK retains ownership of MCP
connections and cleanup. Use one prefixed `SearchableToolset` per authenticated MCP server because
ADK supports one authentication configuration per toolset.

## Public API

### `SearchableToolset`

```python
SearchableToolset(
    *,
    namespace: str,
    tools: Iterable[BaseTool | Callable] = (),
    source: BaseToolset | None = None,
    always_available: Iterable[BaseTool | Callable] = (),
    index_factory: Callable[[], BM25ToolIndex] | None = None,
    top_k: int = 5,
    max_loaded_tools: int = 20,
    tool_name_prefix: str | None = None,
)
```

- `tools`: static deferred tools. Mutually exclusive with `source`.
- `source`: one dynamic ADK toolset, including `McpToolset`.
- `always_available`: tools exposed on every model request but excluded from search.
- `index_factory`: creates an exclusively owned index for each context-specific source snapshot.
- `top_k`: maximum search results.
- `max_loaded_tools`: active deferred-tool budget.
- `tool_name_prefix`: ADK-compatible prefix for management and active tools.

The toolset exposes:

- `search_tools(query)`
- `load_tool(tool_name)`
- `unload_tool(tool_name)`
- `clear_loaded_tools()`

`load_tool` does not execute another tool internally. The selected tool is executed normally by ADK
on a subsequent model step.

### `ToolCatalog`

Normalizes callables to stable `FunctionTool` instances, validates names, rejects duplicates, and
extracts descriptions and parameter metadata.

### `BM25ToolIndex`

Indexes tool names, descriptions, argument names, and argument descriptions. It supports custom
stopwords and minimum token lengths.

### `ToolSearchResult`

Structured retrieval result containing `name`, `description`, and `score`.

## Multiple Sources

Use one searchable toolset per source and prefix each surface:

```python
agent = LlmAgent(
    name="assistant",
    model="gemini-2.5-flash",
    tools=[
        SearchableToolset(namespace="github", source=github_mcp, tool_name_prefix="github"),
        SearchableToolset(namespace="slack", source=slack_mcp, tool_name_prefix="slack"),
    ],
)
```

This preserves independent authentication, lifecycle, catalog refresh, and loaded-tool state.

## Development

```bash
uv sync --all-extras
uv run ruff format --check .
uv run ruff check .
uv run pytest
```

Live model tests require `.env` credentials:

```bash
uv run pytest -m llm
```

The default test suite also starts a local stdio MCP subprocess and exercises real MCP listing,
calling, and cleanup without network access. Live LLM tests use the `ADK_TOOL_SEARCH_LLM_MODEL`,
`ADK_TOOL_SEARCH_LLM_API_BASE`, and `ADK_TOOL_SEARCH_LLM_API_KEY` values from `.env`.
