ToolSearchProcessor
The ToolSearchProcessor is an input processor that enables dynamic tool discovery and loading. Instead of providing all tools to the agent upfront, it gives the agent two meta-tools (search_tools and load_tool) that let it find and load tools on demand. This reduces context token usage when working with large tool libraries.
Usage exampleDirect link to Usage example
import { ToolSearchProcessor } from '@mastra/core/processors'
const toolSearch = new ToolSearchProcessor({
tools: {
createIssue: githubTools.createIssue,
sendEmail: emailTools.send,
getWeather: weatherTools.forecast,
// ... many more tools
},
search: {
topK: 5,
minScore: 0.1,
},
})
Constructor parametersDirect link to Constructor parameters
options:
tools:
search?:
search.topK?:
search.minScore?:
search.autoLoad?:
storage?:
ttl?:
filter?:
ReturnsDirect link to Returns
id:
name:
processInputStep:
MethodsDirect link to Methods
State inspection (legacy 'in-memory' store)Direct link to state-inspection-legacy-in-memory-store
These methods operate on the default 'in-memory' store only. They are no-ops for the 'context' store, whose state lives in the conversation messages rather than an in-process map.
clearState(threadId)Direct link to clearstatethreadid
Clears the loaded-tool state for a single thread.
processor.clearState('thread-123')
clearAllState()Direct link to clearallstate
Clears loaded-tool state for every thread.
processor.clearAllState()
getStateStats()Direct link to getstatestats
Returns the number of tracked threads and the oldest access time, for debugging in-memory growth.
const { threadCount, oldestAccessTime } = processor.getStateStats()
Returns: { threadCount: number; oldestAccessTime: number | null }
cleanupNow()Direct link to cleanupnow
Immediately runs TTL cleanup instead of waiting for the scheduled sweep.
const cleaned = processor.cleanupNow()
Returns: number — the count of threads cleaned up.
Request-aware filteringDirect link to Request-aware filtering
Use filter to apply request-specific policy to dynamic tools. The hook receives the resolved tool ID as toolName, the tool, request context, and phase. toolName is the ID returned by search_tools, which may differ from the key used in the tools object.
import { ToolSearchProcessor } from '@mastra/core/processors'
const toolSearch = new ToolSearchProcessor({
tools: allTools,
filter: ({ toolName, requestContext, phase }) => {
const plan = requestContext?.get('plan')
if (phase === 'search') {
return true
}
return plan === 'pro' || !toolName.startsWith('premium_')
},
})
The phase value describes where the filter is being applied:
search: Filters results returned bysearch_tools.load: Blocksload_toolfrom loading disallowed tools.active: Hides already-loaded tools from the current request if they are no longer allowed.
If the hook throws or rejects, ToolSearchProcessor treats the tool as disallowed for that request. The hook may run for every matching search candidate, so keep async policy checks cheap or cached. The search_tools meta-tool is always available; load_tool is available unless search.autoLoad is enabled. Tools passed directly through the agent or processInputStep remain available unless you filter them outside ToolSearchProcessor.
Extended usage exampleDirect link to Extended usage example
import { Agent } from '@mastra/core/agent'
import { ToolSearchProcessor } from '@mastra/core/processors'
// Tools from various integrations
import { githubTools } from './tools/github'
import { slackTools } from './tools/slack'
import { dbTools } from './tools/database'
const toolSearch = new ToolSearchProcessor({
tools: {
...githubTools, // createIssue, listPRs, mergePR, ...
...slackTools, // sendMessage, createChannel, ...
...dbTools, // query, insert, update, ...
},
search: {
topK: 5,
minScore: 0.1,
},
})
const agent = new Agent({
name: 'dynamic-tools-agent',
instructions:
'You are a helpful assistant with access to many tools. Use search_tools to find relevant tools, then load_tool to make them available.',
model: 'openai/gpt-5.5',
inputProcessors: [toolSearch],
})
The agent workflow is:
- Agent receives a user message
- Agent calls
search_toolswith keywords (e.g., "github issue") - Agent reviews results and calls
load_toolwith the tool name - The loaded tool becomes available on the next turn
- Agent uses the loaded tool normally
Single-step discovery with autoLoadDirect link to single-step-discovery-with-autoload
Set search.autoLoad to true to skip the separate load step. The tools returned by search_tools are activated immediately, and the load_tool meta-tool is not exposed. This removes one model turn per discovery, which lowers token usage and latency, and works the same across providers.
const toolSearch = new ToolSearchProcessor({
tools: allTools,
search: {
topK: 3,
autoLoad: true,
},
})
With autoLoad the workflow becomes:
- Agent receives a user message
- Agent calls
search_toolswith keywords - The matching tools are activated automatically and become available on the next turn
- Agent uses the tool normally
Every match is activated, so keep topK small (for example, 3) to avoid adding tools the agent did not need. Activated tools are appended after existing tools, which keeps the cached prompt prefix stable for providers that support prompt caching.
Loaded-tool storageDirect link to Loaded-tool storage
The storage option controls where the set of loaded tools is tracked. The default is 'in-memory'; the 'context' store is opt-in.
'in-memory' (default)Direct link to in-memory-default
Loaded tools are tracked in an in-memory map per thread, with TTL-based cleanup controlled by the ttl option (default one hour). This is the original behavior:
- Requires no memory configuration.
- State is lost on process restart.
- Requests with no thread ID share a single
'default'entry.
Use clearState, clearAllState, getStateStats, and cleanupNow to inspect or reset this store.
'context'Direct link to context
Loaded state is derived from the conversation messages: a tool is loaded while a search_tools or load_tool result naming it remains in the messages. This mode:
- Requires no memory configuration.
- Is restart-safe — the durable record is the persisted message history.
- De-loads a tool automatically once that result is no longer present in the messages.
import { ToolSearchProcessor } from '@mastra/core/processors'
const toolSearch = new ToolSearchProcessor({
tools: allTools,
storage: 'context',
})
Loading tools is cache-friendly in both modes: loads are append-only, so the cached prompt prefix stays stable for providers that support prompt caching.
Unloading a tool changes the tool definitions sent to the model, which shifts the cached prefix and causes the next turn to pay a cache write instead of a cache hit. In 'in-memory' mode this happens when a thread's state is evicted by ttl. In 'context' mode it happens when a tool's discovery result is no longer present in the messages (for example, when older messages are trimmed) — the tool de-loads and the model must search for it again before reuse. This is expected: removing an unused tool trades one cache write for a smaller prefix on later turns.
Combining with other processorsDirect link to Combining with other processors
import { Agent } from '@mastra/core/agent'
import { ToolSearchProcessor, TokenLimiter } from '@mastra/core/processors'
const agent = new Agent({
name: 'my-agent',
model: 'openai/gpt-5.5',
inputProcessors: [
new ToolSearchProcessor({
tools: allTools,
search: { topK: 5 },
}),
// Place TokenLimiter last to ensure context fits
new TokenLimiter(127000),
],
})