-
Notifications
You must be signed in to change notification settings - Fork 487
Description
Summary
When using callable toolFilter functions that depend on agent or runContext, the module-level cache _cachedTools incorrectly shares filtered tool results across different agents. This causes subsequent agents to receive tools filtered for the first agent, bypassing their own filter logic.
Description
The getFunctionToolsFromServer function in packages/agents-core/src/mcp.ts uses a module-level cache (_cachedTools) keyed only by server.name. However, when callable filters are used, the filtering logic depends on both agent and runContext (see MCPToolFilterContext), which can produce different results for different agents.
Problematic Code Flow
-
Cache lookup (line 312): Only checks
server.nameas the cache keyif (server.cacheToolsList && _cachedTools[server.name]) { return _cachedTools[server.name].map(...); }
-
Filtering logic (lines 322-365): Applies callable filters that depend on
agentandrunContextif (runContext && agent) { const context = { runContext, agent, serverName: server.name }; // ... filter logic that can produce different results per agent }
-
Cache storage (line 373): Stores filtered results using only
server.nameas keyif (server.cacheToolsList) { _cachedTools[server.name] = mcpTools; // ❌ Missing agent/context in key }
Impact
- Severity: Medium to High
- Affected Users: Users who:
- Use multiple agents with the same MCP server
- Use callable
toolFilterfunctions that depend on agent-specific properties - Rely on different agents having different tool access permissions
Steps to Reproduce
import { Agent } from '@openai/agents-core';
import { MCPServerStdio, getAllMcpTools } from '@openai/agents-core';
import { RunContext } from '@openai/agents-core';
// Create an MCP server with a callable filter
const server = new MCPServerStdio({
command: 'some-mcp-server',
cacheToolsList: true,
toolFilter: async (context, tool) => {
// Filter based on agent name
return context.agent.name === 'AgentA'
? tool.name !== 'restricted_tool'
: tool.name !== 'another_restricted_tool';
},
});
// First agent with different filter logic
const agentA = new Agent({
name: 'AgentA',
mcpServers: [server],
});
// Second agent with different filter logic
const agentB = new Agent({
name: 'AgentB',
mcpServers: [server],
});
// First call - AgentA filters tools
const runContextA = new RunContext({});
const toolsA = await getAllMcpTools({
mcpServers: [server],
runContext: runContextA,
agent: agentA,
});
// toolsA is correctly filtered for AgentA
// Second call - AgentB should get different filtered tools
// but receives AgentA's filtered results due to cache
const runContextB = new RunContext({});
const toolsB = await getAllMcpTools({
mcpServers: [server],
runContext: runContextB,
agent: agentB,
});
// ❌ toolsB incorrectly contains AgentA's filtered toolsExpected Behavior
Each agent should receive tools filtered according to its own toolFilter logic, even when sharing the same MCP server instance.
Actual Behavior
After the first agent's tools are cached, subsequent agents receive the first agent's filtered tools, bypassing their own filter logic.
Environment
- Package:
@openai/agents-core - File:
packages/agents-core/src/mcp.ts - Lines: 312-373
Proposed Solutions
Option 1: Include Context in Cache Key (Recommended)
Modify the cache key to include agent identifier when callable filters are present:
// Generate cache key that includes agent context for callable filters
const getCacheKey = (server: MCPServer, agent?: Agent<any, any>) => {
if (server.toolFilter && typeof server.toolFilter === 'function' && agent) {
return `${server.name}:${agent.name}`; // or use a more unique identifier
}
return server.name;
};
// Use in cache lookup and storage
const cacheKey = getCacheKey(server, agent);
if (server.cacheToolsList && _cachedTools[cacheKey]) {
return _cachedTools[cacheKey].map(...);
}
// ...
if (server.cacheToolsList) {
_cachedTools[cacheKey] = mcpTools;
}Option 2: Disable Cache for Callable Filters
Skip caching when callable filters are used:
if (server.cacheToolsList &&
!(server.toolFilter && typeof server.toolFilter === 'function') &&
_cachedTools[server.name]) {
return _cachedTools[server.name].map(...);
}Option 3: Cache Unfiltered Tools, Re-apply Filters
Cache the unfiltered tool list and always re-apply filters:
// Cache unfiltered tools
if (server.cacheToolsList) {
_cachedTools[server.name] = fetchedMcpTools; // Store unfiltered
}
// Always re-apply filters when returning
// (filters are already applied above, but this ensures consistency)Additional Notes
- Static filters (
MCPToolFilterStatic) are not affected since they don't depend on agent/context - The instance-level cache (
this._cachedToolsinMCPServerStdio) works correctly as it's per-instance - This issue only affects the module-level cache used by
getAllMcpTools()
Related Code
packages/agents-core/src/mcp.ts: Lines 289, 312-316, 322-365, 372-374packages/agents-core/src/mcpUtil.ts:MCPToolFilterContextinterfacepackages/agents-core/src/agent.ts:getMcpTools()method (line 640)