@@ -110,4 +110,208 @@ describe('MCP tools cache invalidation', () => {
110110 expect ( called ) . toBe ( true ) ;
111111 } ) ;
112112 } ) ;
113+
114+ it ( 'clears agent-specific cache entries when cache is invalidated' , async ( ) => {
115+ await withTrace ( 'test' , async ( ) => {
116+ const toolsInitial = [
117+ {
118+ name : 'foo_initial' ,
119+ description : '' ,
120+ inputSchema : { type : 'object' , properties : { } } ,
121+ } ,
122+ {
123+ name : 'bar_initial' ,
124+ description : '' ,
125+ inputSchema : { type : 'object' , properties : { } } ,
126+ } ,
127+ ] ;
128+ const toolsUpdated = [
129+ {
130+ name : 'foo_updated' ,
131+ description : '' ,
132+ inputSchema : { type : 'object' , properties : { } } ,
133+ } ,
134+ {
135+ name : 'bar_updated' ,
136+ description : '' ,
137+ inputSchema : { type : 'object' , properties : { } } ,
138+ } ,
139+ ] ;
140+
141+ const server = new StubServer ( 'server' , toolsInitial ) ;
142+ server . toolFilter = async ( ctx : any , tool : any ) => {
143+ if ( ctx . agent . name === 'AgentOne' ) {
144+ return tool . name . startsWith ( 'foo' ) ;
145+ }
146+ return tool . name . startsWith ( 'bar' ) ;
147+ } ;
148+
149+ const agentOne = new Agent ( { name : 'AgentOne' } ) ;
150+ const agentTwo = new Agent ( { name : 'AgentTwo' } ) ;
151+ const ctxOne = new RunContext ( { } ) ;
152+ const ctxTwo = new RunContext ( { } ) ;
153+
154+ const initialToolsAgentOne = await getAllMcpTools ( {
155+ mcpServers : [ server ] ,
156+ runContext : ctxOne ,
157+ agent : agentOne ,
158+ } ) ;
159+ expect ( initialToolsAgentOne . map ( ( t : any ) => t . name ) ) . toEqual ( [
160+ 'foo_initial' ,
161+ ] ) ;
162+
163+ const initialToolsAgentTwo = await getAllMcpTools ( {
164+ mcpServers : [ server ] ,
165+ runContext : ctxTwo ,
166+ agent : agentTwo ,
167+ } ) ;
168+ expect ( initialToolsAgentTwo . map ( ( t : any ) => t . name ) ) . toEqual ( [
169+ 'bar_initial' ,
170+ ] ) ;
171+
172+ server . toolList = toolsUpdated ;
173+ await server . invalidateToolsCache ( ) ;
174+
175+ const updatedToolsAgentOne = await getAllMcpTools ( {
176+ mcpServers : [ server ] ,
177+ runContext : ctxOne ,
178+ agent : agentOne ,
179+ } ) ;
180+ expect ( updatedToolsAgentOne . map ( ( t : any ) => t . name ) ) . toEqual ( [
181+ 'foo_updated' ,
182+ ] ) ;
183+
184+ const updatedToolsAgentTwo = await getAllMcpTools ( {
185+ mcpServers : [ server ] ,
186+ runContext : ctxTwo ,
187+ agent : agentTwo ,
188+ } ) ;
189+ expect ( updatedToolsAgentTwo . map ( ( t : any ) => t . name ) ) . toEqual ( [
190+ 'bar_updated' ,
191+ ] ) ;
192+ } ) ;
193+ } ) ;
194+ } ) ;
195+
196+ describe ( 'MCP tools agent-dependent cache behavior' , ( ) => {
197+ it ( 'handles agent-specific callable tool filters without cache leaking between agents' , async ( ) => {
198+ await withTrace ( 'test' , async ( ) => {
199+ const tools = [
200+ {
201+ name : 'foo' ,
202+ description : '' ,
203+ inputSchema : { type : 'object' , properties : { } } ,
204+ } ,
205+ {
206+ name : 'bar' ,
207+ description : '' ,
208+ inputSchema : { type : 'object' , properties : { } } ,
209+ } ,
210+ ] ;
211+
212+ // Callable filter chooses tool availability per agent name
213+ const filter = async ( ctx : any , tool : any ) => {
214+ if ( ctx . agent . name === 'AgentOne' ) {
215+ return tool . name === 'foo' ; // AgentOne: only 'foo' allowed
216+ } else {
217+ return tool . name === 'bar' ; // AgentTwo: only 'bar' allowed
218+ }
219+ } ;
220+ const server = new StubServer ( 'shared-server' , tools ) ;
221+ server . toolFilter = filter ;
222+
223+ const agentOne = new Agent ( { name : 'AgentOne' } ) ;
224+ const agentTwo = new Agent ( { name : 'AgentTwo' } ) ;
225+ const ctxOne = new RunContext ( { } ) ;
226+ const ctxTwo = new RunContext ( { } ) ;
227+
228+ // First access by AgentOne (should get only 'foo')
229+ const result1 = await getAllMcpTools ( {
230+ mcpServers : [ server ] ,
231+ runContext : ctxOne ,
232+ agent : agentOne ,
233+ } ) ;
234+ expect ( result1 . map ( ( t : any ) => t . name ) ) . toEqual ( [ 'foo' ] ) ;
235+
236+ // Second access by AgentTwo (should get only 'bar')
237+ const result2 = await getAllMcpTools ( {
238+ mcpServers : [ server ] ,
239+ runContext : ctxTwo ,
240+ agent : agentTwo ,
241+ } ) ;
242+ expect ( result2 . map ( ( t : any ) => t . name ) ) . toEqual ( [ 'bar' ] ) ;
243+
244+ // Third access by AgentOne (should still get only 'foo', from cache key)
245+ const result3 = await getAllMcpTools ( {
246+ mcpServers : [ server ] ,
247+ runContext : ctxOne ,
248+ agent : agentOne ,
249+ } ) ;
250+ expect ( result3 . map ( ( t : any ) => t . name ) ) . toEqual ( [ 'foo' ] ) ;
251+ } ) ;
252+ } ) ;
253+ } ) ;
254+
255+ describe ( 'Custom generateMCPToolCacheKey can include runContext in key' , ( ) => {
256+ it ( 'supports fully custom cache key logic, including runContext properties' , async ( ) => {
257+ await withTrace ( 'test' , async ( ) => {
258+ const tools = [
259+ {
260+ name : 'foo' ,
261+ description : '' ,
262+ inputSchema : { type : 'object' , properties : { } } ,
263+ } ,
264+ {
265+ name : 'bar' ,
266+ description : '' ,
267+ inputSchema : { type : 'object' , properties : { } } ,
268+ } ,
269+ ] ;
270+ // Filter that allows a tool based on runContext meta value
271+ const filter = async ( ctx : any , tool : any ) => {
272+ if ( ctx . runContext . meta && ctx . runContext . meta . kind === 'fooUser' ) {
273+ return tool . name === 'foo' ;
274+ } else {
275+ return tool . name === 'bar' ;
276+ }
277+ } ;
278+ const server = new StubServer ( 'custom-key-srv' , tools ) ;
279+ server . toolFilter = filter ;
280+ const agent = new Agent ( { name : 'A' } ) ;
281+ // This cache key generator uses both agent name and runContext.meta.kind
282+ const generateMCPToolCacheKey = ( { server, agent, runContext } : any ) =>
283+ `${ server . name } :${ agent ? agent . name : '' } :${ runContext ?. meta ?. kind } ` ;
284+
285+ // Agent 'A', runContext kind 'fooUser' => should see only 'foo'
286+ const context1 = new RunContext ( { } ) ;
287+ ( context1 as any ) . meta = { kind : 'fooUser' } ;
288+ const res1 = await getAllMcpTools ( {
289+ mcpServers : [ server ] ,
290+ runContext : context1 ,
291+ agent,
292+ generateMCPToolCacheKey,
293+ } ) ;
294+ expect ( res1 . map ( ( t : any ) => t . name ) ) . toEqual ( [ 'foo' ] ) ;
295+
296+ // Agent 'A', runContext kind 'barUser' => should see only 'bar'
297+ const context2 = new RunContext ( { } ) ;
298+ ( context2 as any ) . meta = { kind : 'barUser' } ;
299+ const res2 = await getAllMcpTools ( {
300+ mcpServers : [ server ] ,
301+ runContext : context2 ,
302+ agent,
303+ generateMCPToolCacheKey,
304+ } ) ;
305+ expect ( res2 . map ( ( t : any ) => t . name ) ) . toEqual ( [ 'bar' ] ) ;
306+
307+ // Agent 'A'/'fooUser' again => should hit the correct cache entry, still see only 'foo'
308+ const res3 = await getAllMcpTools ( {
309+ mcpServers : [ server ] ,
310+ runContext : context1 ,
311+ agent,
312+ generateMCPToolCacheKey,
313+ } ) ;
314+ expect ( res3 . map ( ( t : any ) => t . name ) ) . toEqual ( [ 'foo' ] ) ;
315+ } ) ;
316+ } ) ;
113317} ) ;
0 commit comments