Skip to content

MCP Tools Cache Incorrectly Shared Across Different Agents with Callable Filters #695

@likebean

Description

@likebean

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

  1. Cache lookup (line 312): Only checks server.name as the cache key

    if (server.cacheToolsList && _cachedTools[server.name]) {
      return _cachedTools[server.name].map(...);
    }
  2. Filtering logic (lines 322-365): Applies callable filters that depend on agent and runContext

    if (runContext && agent) {
      const context = { runContext, agent, serverName: server.name };
      // ... filter logic that can produce different results per agent
    }
  3. Cache storage (line 373): Stores filtered results using only server.name as key

    if (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 toolFilter functions 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 tools

Expected 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._cachedTools in MCPServerStdio) 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-374
  • packages/agents-core/src/mcpUtil.ts: MCPToolFilterContext interface
  • packages/agents-core/src/agent.ts: getMcpTools() method (line 640)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions