Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/social-sloths-make.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-core': patch
---

feat: #695 Customizable MCP list tool caching
1 change: 1 addition & 0 deletions packages/agents-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export {
MCPServerStreamableHttp,
MCPServerSSE,
GetAllMcpToolsOptions,
MCPToolCacheKeyGenerator,
} from './mcp';
export {
MCPToolFilterCallable,
Expand Down
49 changes: 40 additions & 9 deletions packages/agents-core/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,31 @@ const _cachedTools: Record<string, MCPTool[]> = {};
export async function invalidateServerToolsCache(serverName: string) {
delete _cachedTools[serverName];
}

/**
* Function signature for generating the MCP tool cache key.
* Customizable so the cache key can depend on any context—server, agent, runContext, etc.
*/
export type MCPToolCacheKeyGenerator = (params: {
server: MCPServer;
agent?: Agent<any, any>;
runContext?: RunContext<any>;
}) => string;

/**
* Default cache key generator for MCP tools.
* Uses server name, or server+agent if using callable filter.
*/
export const defaultMCPToolCacheKey: MCPToolCacheKeyGenerator = ({
server,
agent,
}) => {
if (server.toolFilter && typeof server.toolFilter === 'function' && agent) {
return `${server.name}:${agent.name}`;
}
return server.name;
};

/**
* Fetches all function tools from a single MCP server.
*/
Expand All @@ -309,14 +334,22 @@ async function getFunctionToolsFromServer<TContext = UnknownContext>({
convertSchemasToStrict,
runContext,
agent,
generateMCPToolCacheKey,
}: {
server: MCPServer;
convertSchemasToStrict: boolean;
runContext?: RunContext<TContext>;
agent?: Agent<any, any>;
generateMCPToolCacheKey?: MCPToolCacheKeyGenerator;
}): Promise<FunctionTool<TContext, any, unknown>[]> {
if (server.cacheToolsList && _cachedTools[server.name]) {
return _cachedTools[server.name].map((t) =>
const cacheKey = (generateMCPToolCacheKey || defaultMCPToolCacheKey)({
server,
agent,
runContext,
});
// Use cache key generator injected from the outside, or the default if absent.
if (server.cacheToolsList && _cachedTools[cacheKey]) {
return _cachedTools[cacheKey].map((t) =>
mcpToFunctionTool(t, server, convertSchemasToStrict),
);
}
Expand Down Expand Up @@ -379,8 +412,9 @@ async function getFunctionToolsFromServer<TContext = UnknownContext>({
const tools: FunctionTool<TContext, any, string>[] = mcpTools.map((t) =>
mcpToFunctionTool(t, server, convertSchemasToStrict),
);
// Cache store
if (server.cacheToolsList) {
_cachedTools[server.name] = mcpTools;
_cachedTools[cacheKey] = mcpTools;
}
return tools;
};
Expand All @@ -402,18 +436,13 @@ export type GetAllMcpToolsOptions<TContext> = {
convertSchemasToStrict?: boolean;
runContext?: RunContext<TContext>;
agent?: Agent<TContext, any>;
generateMCPToolCacheKey?: MCPToolCacheKeyGenerator;
};

/**
* Returns all MCP tools from the provided servers, using the function tool conversion.
* If runContext and agent are provided, callable tool filters will be applied.
*/
export async function getAllMcpTools<TContext = UnknownContext>(
mcpServers: MCPServer[],
): Promise<Tool<TContext>[]>;
export async function getAllMcpTools<TContext = UnknownContext>(
opts: GetAllMcpToolsOptions<TContext>,
): Promise<Tool<TContext>[]>;
export async function getAllMcpTools<TContext = UnknownContext>(
mcpServersOrOpts: MCPServer[] | GetAllMcpToolsOptions<TContext>,
runContext?: RunContext<TContext>,
Expand All @@ -434,6 +463,7 @@ export async function getAllMcpTools<TContext = UnknownContext>(
convertSchemasToStrict: convertSchemasToStrictFromOpts = false,
runContext: runContextFromOpts,
agent: agentFromOpts,
generateMCPToolCacheKey,
} = opts;
const allTools: Tool<TContext>[] = [];
const toolNames = new Set<string>();
Expand All @@ -444,6 +474,7 @@ export async function getAllMcpTools<TContext = UnknownContext>(
convertSchemasToStrict: convertSchemasToStrictFromOpts,
runContext: runContextFromOpts,
agent: agentFromOpts,
generateMCPToolCacheKey,
});
const serverToolNames = new Set(serverTools.map((t) => t.name));
const intersection = [...serverToolNames].filter((n) => toolNames.has(n));
Expand Down
123 changes: 123 additions & 0 deletions packages/agents-core/test/mcpCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,126 @@ describe('MCP tools cache invalidation', () => {
});
});
});

describe('MCP tools agent-dependent cache behavior', () => {
it('handles agent-specific callable tool filters without cache leaking between agents', async () => {
await withTrace('test', async () => {
const tools = [
{
name: 'foo',
description: '',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'bar',
description: '',
inputSchema: { type: 'object', properties: {} },
},
];

// Callable filter chooses tool availability per agent name
const filter = async (ctx: any, tool: any) => {
if (ctx.agent.name === 'AgentOne') {
return tool.name === 'foo'; // AgentOne: only 'foo' allowed
} else {
return tool.name === 'bar'; // AgentTwo: only 'bar' allowed
}
};
const server = new StubServer('shared-server', tools);
server.toolFilter = filter;

const agentOne = new Agent({ name: 'AgentOne' });
const agentTwo = new Agent({ name: 'AgentTwo' });
const ctxOne = new RunContext({});
const ctxTwo = new RunContext({});

// First access by AgentOne (should get only 'foo')
const result1 = await getAllMcpTools({
mcpServers: [server],
runContext: ctxOne,
agent: agentOne,
});
expect(result1.map((t: any) => t.name)).toEqual(['foo']);

// Second access by AgentTwo (should get only 'bar')
const result2 = await getAllMcpTools({
mcpServers: [server],
runContext: ctxTwo,
agent: agentTwo,
});
expect(result2.map((t: any) => t.name)).toEqual(['bar']);

// Third access by AgentOne (should still get only 'foo', from cache key)
const result3 = await getAllMcpTools({
mcpServers: [server],
runContext: ctxOne,
agent: agentOne,
});
expect(result3.map((t: any) => t.name)).toEqual(['foo']);
});
});
});

describe('Custom generateMCPToolCacheKey can include runContext in key', () => {
it('supports fully custom cache key logic, including runContext properties', async () => {
await withTrace('test', async () => {
const tools = [
{
name: 'foo',
description: '',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'bar',
description: '',
inputSchema: { type: 'object', properties: {} },
},
];
// Filter that allows a tool based on runContext meta value
const filter = async (ctx: any, tool: any) => {
if (ctx.runContext.meta && ctx.runContext.meta.kind === 'fooUser') {
return tool.name === 'foo';
} else {
return tool.name === 'bar';
}
};
const server = new StubServer('custom-key-srv', tools);
server.toolFilter = filter;
const agent = new Agent({ name: 'A' });
// This cache key generator uses both agent name and runContext.meta.kind
const generateMCPToolCacheKey = ({ server, agent, runContext }: any) =>
`${server.name}:${agent ? agent.name : ''}:${runContext?.meta?.kind}`;

// Agent 'A', runContext kind 'fooUser' => should see only 'foo'
const context1 = new RunContext({});
(context1 as any).meta = { kind: 'fooUser' };
const res1 = await getAllMcpTools({
mcpServers: [server],
runContext: context1,
agent,
generateMCPToolCacheKey,
});
expect(res1.map((t: any) => t.name)).toEqual(['foo']);

// Agent 'A', runContext kind 'barUser' => should see only 'bar'
const context2 = new RunContext({});
(context2 as any).meta = { kind: 'barUser' };
const res2 = await getAllMcpTools({
mcpServers: [server],
runContext: context2,
agent,
generateMCPToolCacheKey,
});
expect(res2.map((t: any) => t.name)).toEqual(['bar']);

// Agent 'A'/'fooUser' again => should hit the correct cache entry, still see only 'foo'
const res3 = await getAllMcpTools({
mcpServers: [server],
runContext: context1,
agent,
generateMCPToolCacheKey,
});
expect(res3.map((t: any) => t.name)).toEqual(['foo']);
});
});
});