From d5f1051574fd63080c20fe8a6fcd877e363d6bc6 Mon Sep 17 00:00:00 2001 From: Viraj <123119434+vrtnis@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:45:58 -0700 Subject: [PATCH 01/14] docs(mcp): document tool filtering --- docs/src/content/docs/guides/mcp.mdx | 17 ++ packages/agents-core/src/agent.ts | 12 +- packages/agents-core/src/index.ts | 6 + packages/agents-core/src/mcp.ts | 55 ++++- packages/agents-core/src/mcpUtil.ts | 46 ++++ packages/agents-core/src/run.ts | 4 +- packages/agents-core/src/runState.ts | 4 +- .../src/shims/mcp-server/browser.ts | 12 +- .../agents-core/src/shims/mcp-server/node.ts | 126 +++++++++-- .../agents-core/test/mcpToolFilter.test.ts | 201 ++++++++++++++++++ .../agents-realtime/src/realtimeSession.ts | 7 +- 11 files changed, 456 insertions(+), 34 deletions(-) create mode 100644 packages/agents-core/src/mcpUtil.ts create mode 100644 packages/agents-core/test/mcpToolFilter.test.ts diff --git a/docs/src/content/docs/guides/mcp.mdx b/docs/src/content/docs/guides/mcp.mdx index f55e9c71..66a1d5a8 100644 --- a/docs/src/content/docs/guides/mcp.mdx +++ b/docs/src/content/docs/guides/mcp.mdx @@ -97,6 +97,23 @@ For **Streamable HTTP** and **Stdio** servers, each time an `Agent` runs it may Only enable this if you're confident the tool list won't change. To invalidate the cache later, call `invalidateToolsCache()` on the server instance. +### Tool filtering + +You can restrict which tools are exposed from each server. Pass either a static filter +using `createStaticToolFilter` or a custom function: + +```ts +const server = new MCPServerStdio({ + fullCommand: 'my-server', + toolFilter: createStaticToolFilter(['safe_tool'], ['danger_tool']), +}); + +const dynamicServer = new MCPServerStreamableHttp({ + url: 'http://localhost:3000', + toolFilter: ({ runContext }, tool) => runContext.context.allowAll || tool.name !== 'admin', +}); +``` + ## Further reading - [Model Context Protocol](https://modelcontextprotocol.io/) – official specification. diff --git a/packages/agents-core/src/agent.ts b/packages/agents-core/src/agent.ts index c9fcf61e..e5118bbf 100644 --- a/packages/agents-core/src/agent.ts +++ b/packages/agents-core/src/agent.ts @@ -514,9 +514,11 @@ export class Agent< * Fetches the available tools from the MCP servers. * @returns the MCP powered tools */ - async getMcpTools(): Promise[]> { + async getMcpTools( + runContext: RunContext, + ): Promise[]> { if (this.mcpServers.length > 0) { - return getAllMcpTools(this.mcpServers); + return getAllMcpTools(this.mcpServers, false, runContext, this); } return []; @@ -527,8 +529,10 @@ export class Agent< * * @returns all configured tools */ - async getAllTools(): Promise[]> { - return [...(await this.getMcpTools()), ...this.tools]; + async getAllTools( + runContext: RunContext, + ): Promise[]> { + return [...(await this.getMcpTools(runContext)), ...this.tools]; } /** diff --git a/packages/agents-core/src/index.ts b/packages/agents-core/src/index.ts index 00c5f0b4..e5d689b1 100644 --- a/packages/agents-core/src/index.ts +++ b/packages/agents-core/src/index.ts @@ -73,6 +73,12 @@ export { MCPServerStdio, MCPServerStreamableHttp, } from './mcp'; +export { + ToolFilterCallable, + ToolFilterContext, + ToolFilterStatic, + createStaticToolFilter, +} from './mcpUtil'; export { Model, ModelProvider, diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index cbcf8f23..60c74cc0 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -14,6 +14,9 @@ import { JsonObjectSchemaStrict, UnknownContext, } from './types'; +import type { ToolFilterCallable, ToolFilterStatic } from './mcpUtil'; +import type { RunContext } from './runContext'; +import type { Agent } from './agent'; export const DEFAULT_STDIO_MCP_CLIENT_LOGGER_NAME = 'openai-agents:stdio-mcp-client'; @@ -30,7 +33,10 @@ export interface MCPServer { connect(): Promise; readonly name: string; close(): Promise; - listTools(): Promise; + listTools( + runContext?: RunContext, + agent?: Agent, + ): Promise; callTool( toolName: string, args: Record | null, @@ -40,18 +46,23 @@ export interface MCPServer { export abstract class BaseMCPServerStdio implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; + protected toolFilter?: ToolFilterCallable | ToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStdioOptions) { this.logger = options.logger ?? getLogger(DEFAULT_STDIO_MCP_CLIENT_LOGGER_NAME); this.cacheToolsList = options.cacheToolsList ?? false; + this.toolFilter = options.toolFilter; } abstract get name(): string; abstract connect(): Promise; abstract close(): Promise; - abstract listTools(): Promise; + abstract listTools( + runContext?: RunContext, + agent?: Agent, + ): Promise; abstract callTool( _toolName: string, _args: Record | null, @@ -72,6 +83,7 @@ export abstract class BaseMCPServerStdio implements MCPServer { export abstract class BaseMCPServerStreamableHttp implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; + protected toolFilter?: ToolFilterCallable | ToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStreamableHttpOptions) { @@ -79,12 +91,16 @@ export abstract class BaseMCPServerStreamableHttp implements MCPServer { options.logger ?? getLogger(DEFAULT_STREAMABLE_HTTP_MCP_CLIENT_LOGGER_NAME); this.cacheToolsList = options.cacheToolsList ?? false; + this.toolFilter = options.toolFilter; } abstract get name(): string; abstract connect(): Promise; abstract close(): Promise; - abstract listTools(): Promise; + abstract listTools( + runContext?: RunContext, + agent?: Agent, + ): Promise; abstract callTool( _toolName: string, _args: Record | null, @@ -138,11 +154,14 @@ export class MCPServerStdio extends BaseMCPServerStdio { close(): Promise { return this.underlying.close(); } - async listTools(): Promise { + async listTools( + runContext?: RunContext, + agent?: Agent, + ): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(); + const tools = await this.underlying.listTools(runContext, agent); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -171,11 +190,14 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { close(): Promise { return this.underlying.close(); } - async listTools(): Promise { + async listTools( + runContext?: RunContext, + agent?: Agent, + ): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(); + const tools = await this.underlying.listTools(runContext, agent); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -196,6 +218,8 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { export async function getAllMcpFunctionTools( mcpServers: MCPServer[], convertSchemasToStrict = false, + runContext?: RunContext, + agent?: Agent, ): Promise[]> { const allTools: Tool[] = []; const toolNames = new Set(); @@ -203,6 +227,8 @@ export async function getAllMcpFunctionTools( const serverTools = await getFunctionToolsFromServer( server, convertSchemasToStrict, + runContext, + agent, ); const serverToolNames = new Set(serverTools.map((t) => t.name)); const intersection = [...serverToolNames].filter((n) => toolNames.has(n)); @@ -234,13 +260,15 @@ export function invalidateServerToolsCache(serverName: string) { async function getFunctionToolsFromServer( server: MCPServer, convertSchemasToStrict: boolean, + runContext?: RunContext, + agent?: Agent, ): Promise[]> { if (server.cacheToolsList && _cachedTools[server.name]) { return _cachedTools[server.name]; } return withMCPListToolsSpan( async (span) => { - const mcpTools = await server.listTools(); + const mcpTools = await server.listTools(runContext, agent); span.spanData.result = mcpTools.map((t) => t.name); const tools: FunctionTool[] = mcpTools.map((t) => mcpToFunctionTool(t, server, convertSchemasToStrict), @@ -260,8 +288,15 @@ async function getFunctionToolsFromServer( export async function getAllMcpTools( mcpServers: MCPServer[], convertSchemasToStrict = false, + runContext?: RunContext, + agent?: Agent, ): Promise[]> { - return getAllMcpFunctionTools(mcpServers, convertSchemasToStrict); + return getAllMcpFunctionTools( + mcpServers, + convertSchemasToStrict, + runContext, + agent, + ); } /** @@ -351,6 +386,7 @@ export interface BaseMCPServerStdioOptions { encoding?: string; encodingErrorHandler?: 'strict' | 'ignore' | 'replace'; logger?: Logger; + toolFilter?: ToolFilterCallable | ToolFilterStatic; } export interface DefaultMCPServerStdioOptions extends BaseMCPServerStdioOptions { @@ -371,6 +407,7 @@ export interface MCPServerStreamableHttpOptions { clientSessionTimeoutSeconds?: number; name?: string; logger?: Logger; + toolFilter?: ToolFilterCallable | ToolFilterStatic; // ---------------------------------------------------- // OAuth diff --git a/packages/agents-core/src/mcpUtil.ts b/packages/agents-core/src/mcpUtil.ts new file mode 100644 index 00000000..8496043f --- /dev/null +++ b/packages/agents-core/src/mcpUtil.ts @@ -0,0 +1,46 @@ +import type { Agent } from './agent'; +import type { RunContext } from './runContext'; +import type { MCPTool } from './mcp'; +import type { UnknownContext } from './types'; + +/** Context information available to tool filter functions. */ +export interface ToolFilterContext { + /** The current run context. */ + runContext: RunContext; + /** The agent requesting the tools. */ + agent: Agent; + /** Name of the MCP server providing the tools. */ + serverName: string; +} + +/** A function that determines whether a tool should be available. */ +export type ToolFilterCallable = ( + context: ToolFilterContext, + tool: MCPTool, +) => boolean | Promise; + +/** Static tool filter configuration using allow and block lists. */ +export interface ToolFilterStatic { + /** Optional list of tool names to allow. */ + allowedToolNames?: string[]; + /** Optional list of tool names to block. */ + blockedToolNames?: string[]; +} + +/** Convenience helper to create a static tool filter. */ +export function createStaticToolFilter( + allowedToolNames?: string[], + blockedToolNames?: string[], +): ToolFilterStatic | undefined { + if (!allowedToolNames && !blockedToolNames) { + return undefined; + } + const filter: ToolFilterStatic = {}; + if (allowedToolNames) { + filter.allowedToolNames = allowedToolNames; + } + if (blockedToolNames) { + filter.blockedToolNames = blockedToolNames; + } + return filter; +} diff --git a/packages/agents-core/src/run.ts b/packages/agents-core/src/run.ts index a0c4c4a4..af8cc533 100644 --- a/packages/agents-core/src/run.ts +++ b/packages/agents-core/src/run.ts @@ -322,7 +322,7 @@ export class Runner extends RunHooks> { setCurrentSpan(state._currentAgentSpan); } - const tools = await state._currentAgent.getAllTools(); + const tools = await state._currentAgent.getAllTools(state._context); const serializedTools = tools.map((t) => serializeTool(t)); const serializedHandoffs = handoffs.map((h) => serializeHandoff(h)); if (state._currentAgentSpan) { @@ -615,7 +615,7 @@ export class Runner extends RunHooks> { while (true) { const currentAgent = result.state._currentAgent; const handoffs = currentAgent.handoffs.map(getHandoff); - const tools = await currentAgent.getAllTools(); + const tools = await currentAgent.getAllTools(result.state._context); const serializedTools = tools.map((t) => serializeTool(t)); const serializedHandoffs = handoffs.map((h) => serializeHandoff(h)); diff --git a/packages/agents-core/src/runState.ts b/packages/agents-core/src/runState.ts index 449cd79b..54abbd5e 100644 --- a/packages/agents-core/src/runState.ts +++ b/packages/agents-core/src/runState.ts @@ -558,6 +558,7 @@ export class RunState> { agentMap, state._currentAgent, stateJson.lastProcessedResponse, + state._context, ) : undefined; @@ -710,8 +711,9 @@ async function deserializeProcessedResponse( serializedProcessedResponse: z.infer< typeof serializedProcessedResponseSchema >, + runContext: RunContext, ): Promise> { - const allTools = await currentAgent.getAllTools(); + const allTools = await currentAgent.getAllTools(runContext); const tools = new Map( allTools .filter((tool) => tool.type === 'function') diff --git a/packages/agents-core/src/shims/mcp-server/browser.ts b/packages/agents-core/src/shims/mcp-server/browser.ts index 7d9e7fbc..712b0808 100644 --- a/packages/agents-core/src/shims/mcp-server/browser.ts +++ b/packages/agents-core/src/shims/mcp-server/browser.ts @@ -6,6 +6,8 @@ import { MCPServerStreamableHttpOptions, MCPTool, } from '../../mcp'; +import type { RunContext } from '../../runContext'; +import type { Agent } from '../../agent'; export class MCPServerStdio extends BaseMCPServerStdio { constructor(params: MCPServerStdioOptions) { @@ -20,7 +22,10 @@ export class MCPServerStdio extends BaseMCPServerStdio { close(): Promise { throw new Error('Method not implemented.'); } - listTools(): Promise { + listTools( + _runContext?: RunContext, + _agent?: Agent, + ): Promise { throw new Error('Method not implemented.'); } callTool( @@ -44,7 +49,10 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { close(): Promise { throw new Error('Method not implemented.'); } - listTools(): Promise { + listTools( + _runContext?: RunContext, + _agent?: Agent, + ): Promise { throw new Error('Method not implemented.'); } callTool( diff --git a/packages/agents-core/src/shims/mcp-server/node.ts b/packages/agents-core/src/shims/mcp-server/node.ts index 42fc6707..00752c80 100644 --- a/packages/agents-core/src/shims/mcp-server/node.ts +++ b/packages/agents-core/src/shims/mcp-server/node.ts @@ -12,6 +12,9 @@ import { invalidateServerToolsCache, } from '../../mcp'; import logger from '../../logger'; +import type { ToolFilterContext } from '../../mcpUtil'; +import type { RunContext } from '../../runContext'; +import type { Agent } from '../../agent'; export interface SessionMessage { message: any; @@ -97,7 +100,52 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio { } // The response element type is intentionally left as `any` to avoid explosing MCP SDK type dependencies. - async listTools(): Promise { + protected async _applyToolFilter( + tools: MCPTool[], + runContext?: RunContext, + agent?: Agent, + ): Promise { + if (!this.toolFilter) { + return tools; + } + + if (typeof this.toolFilter === 'function') { + const ctx = { + runContext: runContext as RunContext, + agent: agent as Agent, + serverName: this.name, + } as ToolFilterContext; + const filtered: MCPTool[] = []; + for (const t of tools) { + try { + const res = this.toolFilter(ctx, t); + const include = res instanceof Promise ? await res : res; + if (include) filtered.push(t); + } catch (e) { + this.logger.error( + `Error applying tool filter to tool '${t.name}' on server '${this.name}': ${e}`, + ); + } + } + return filtered; + } + + let filtered = tools; + if (this.toolFilter.allowedToolNames) { + const allowed = new Set(this.toolFilter.allowedToolNames); + filtered = filtered.filter((t) => allowed.has(t.name)); + } + if (this.toolFilter.blockedToolNames) { + const blocked = new Set(this.toolFilter.blockedToolNames); + filtered = filtered.filter((t) => !blocked.has(t.name)); + } + return filtered; + } + + async listTools( + runContext?: RunContext, + agent?: Agent, + ): Promise { const { ListToolsResultSchema } = await import( '@modelcontextprotocol/sdk/types.js' ).catch(failedToImport); @@ -106,14 +154,17 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio { 'Server not initialized. Make sure you call connect() first.', ); } + let tools: MCPTool[]; if (this.cacheToolsList && !this._cacheDirty && this._toolsList) { - return this._toolsList; + tools = this._toolsList; + } else { + this._cacheDirty = false; + const response = await this.session.listTools(); + this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); + this._toolsList = ListToolsResultSchema.parse(response).tools; + tools = this._toolsList; } - this._cacheDirty = false; - const response = await this.session.listTools(); - this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); - this._toolsList = ListToolsResultSchema.parse(response).tools; - return this._toolsList; + return this._applyToolFilter(tools, runContext, agent); } async callTool( @@ -214,7 +265,51 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp { } // The response element type is intentionally left as `any` to avoid explosing MCP SDK type dependencies. - async listTools(): Promise { + protected async _applyToolFilter( + tools: MCPTool[], + runContext?: RunContext, + agent?: Agent, + ): Promise { + if (!this.toolFilter) { + return tools; + } + if (typeof this.toolFilter === 'function') { + const ctx: ToolFilterContext = { + runContext: runContext as RunContext, + agent: agent as Agent, + serverName: this.name, + }; + const filtered: MCPTool[] = []; + for (const t of tools) { + try { + const res = this.toolFilter(ctx, t); + const include = res instanceof Promise ? await res : res; + if (include) filtered.push(t); + } catch (e) { + this.logger.error( + `Error applying tool filter to tool '${t.name}' on server '${this.name}': ${e}`, + ); + } + } + return filtered; + } + + let filtered = tools; + if (this.toolFilter.allowedToolNames) { + const allowed = new Set(this.toolFilter.allowedToolNames); + filtered = filtered.filter((t) => allowed.has(t.name)); + } + if (this.toolFilter.blockedToolNames) { + const blocked = new Set(this.toolFilter.blockedToolNames); + filtered = filtered.filter((t) => !blocked.has(t.name)); + } + return filtered; + } + + async listTools( + runContext?: RunContext, + agent?: Agent, + ): Promise { const { ListToolsResultSchema } = await import( '@modelcontextprotocol/sdk/types.js' ).catch(failedToImport); @@ -223,14 +318,17 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp { 'Server not initialized. Make sure you call connect() first.', ); } + let tools: MCPTool[]; if (this.cacheToolsList && !this._cacheDirty && this._toolsList) { - return this._toolsList; + tools = this._toolsList; + } else { + this._cacheDirty = false; + const response = await this.session.listTools(); + this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); + this._toolsList = ListToolsResultSchema.parse(response).tools; + tools = this._toolsList; } - this._cacheDirty = false; - const response = await this.session.listTools(); - this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); - this._toolsList = ListToolsResultSchema.parse(response).tools; - return this._toolsList; + return this._applyToolFilter(tools, runContext, agent); } async callTool( diff --git a/packages/agents-core/test/mcpToolFilter.test.ts b/packages/agents-core/test/mcpToolFilter.test.ts new file mode 100644 index 00000000..6823a274 --- /dev/null +++ b/packages/agents-core/test/mcpToolFilter.test.ts @@ -0,0 +1,201 @@ +import { describe, it, expect } from 'vitest'; +import { withTrace } from '../src/tracing'; +import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; +import { createStaticToolFilter } from '../src/mcpUtil'; +import { Agent } from '../src/agent'; +import { RunContext } from '../src/runContext'; + +class StubServer extends NodeMCPServerStdio { + public toolList: any[]; + constructor(name: string, tools: any[], filter?: any) { + super({ command: 'noop', name, toolFilter: filter, cacheToolsList: true }); + this.toolList = tools; + this.session = { + listTools: async () => ({ tools: this.toolList }), + callTool: async () => [], + close: async () => {}, + } as any; + this._cacheDirty = true; + } + async connect() {} + async close() {} +} + +describe('MCP tool filtering', () => { + it('static allow/block lists', async () => { + await withTrace('test', async () => { + const tools = [ + { + name: 'a', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'b', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + const server = new StubServer( + 's', + tools, + createStaticToolFilter(['a'], ['b']), + ); + const agent = new Agent({ + name: 'agent', + instructions: '', + model: '', + modelSettings: {}, + tools: [], + mcpServers: [], + }); + const runContext = new RunContext(); + const result = await server.listTools(runContext, agent); + expect(result.map((t) => t.name)).toEqual(['a']); + }); + }); + + it('callable filter functions', async () => { + await withTrace('test', async () => { + const tools = [ + { + name: 'good', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'bad', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + const filter = (_ctx: any, tool: any) => tool.name !== 'bad'; + const server = new StubServer('s', tools, filter); + const agent = new Agent({ + name: 'agent', + instructions: '', + model: '', + modelSettings: {}, + tools: [], + mcpServers: [], + }); + const runContext = new RunContext(); + const result = await server.listTools(runContext, agent); + expect(result.map((t) => t.name)).toEqual(['good']); + }); + }); + + it('hierarchy across multiple servers', async () => { + await withTrace('test', async () => { + const toolsA = [ + { + name: 'a1', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'a2', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + const toolsB = [ + { + name: 'b1', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + const serverA = new StubServer( + 'A', + toolsA, + createStaticToolFilter(['a1']), + ); + const serverB = new StubServer('B', toolsB); + const agent = new Agent({ + name: 'agent', + instructions: '', + model: '', + modelSettings: {}, + tools: [], + mcpServers: [], + }); + const runContext = new RunContext(); + const resultA = await serverA.listTools(runContext, agent); + const resultB = await serverB.listTools(runContext, agent); + expect(resultA.map((t) => t.name)).toEqual(['a1']); + expect(resultB.map((t) => t.name)).toEqual(['b1']); + }); + }); + + it('cache interaction with filtering', async () => { + await withTrace('test', async () => { + const tools = [ + { + name: 'x', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + const server = new StubServer( + 'cache', + tools, + createStaticToolFilter(['x']), + ); + const agent = new Agent({ + name: 'agent', + instructions: '', + model: '', + modelSettings: {}, + tools: [], + mcpServers: [], + }); + const runContext = new RunContext(); + let result = await server.listTools(runContext, agent); + expect(result.map((t) => t.name)).toEqual(['x']); + server.toolFilter = createStaticToolFilter(['y']); + result = await server.listTools(runContext, agent); + expect(result.map((t) => t.name)).toEqual([]); + }); + }); +}); diff --git a/packages/agents-realtime/src/realtimeSession.ts b/packages/agents-realtime/src/realtimeSession.ts index 62738ac7..3b4631bf 100644 --- a/packages/agents-realtime/src/realtimeSession.ts +++ b/packages/agents-realtime/src/realtimeSession.ts @@ -275,7 +275,7 @@ export class RealtimeSession< handoff.getHandoffAsFunctionTool(), ); this.#currentTools = [ - ...(await this.#currentAgent.getAllTools()).filter( + ...(await this.#currentAgent.getAllTools(this.#context)).filter( (tool) => tool.type === 'function', ), ...handoffTools, @@ -445,7 +445,10 @@ export class RealtimeSession< ); const functionToolMap = new Map( - (await this.#currentAgent.getAllTools()).map((tool) => [tool.name, tool]), + (await this.#currentAgent.getAllTools(this.#context)).map((tool) => [ + tool.name, + tool, + ]), ); const possibleHandoff = handoffMap.get(toolCall.name); From 461acb7d231018de5ae45c53456ced6e5a3e10fb Mon Sep 17 00:00:00 2001 From: Viraj <123119434+vrtnis@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:09:31 -0700 Subject: [PATCH 02/14] fix(realtime): narrow currentAgent type for getAllTools --- packages/agents-realtime/src/realtimeSession.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/agents-realtime/src/realtimeSession.ts b/packages/agents-realtime/src/realtimeSession.ts index 3b4631bf..55586314 100644 --- a/packages/agents-realtime/src/realtimeSession.ts +++ b/packages/agents-realtime/src/realtimeSession.ts @@ -274,10 +274,11 @@ export class RealtimeSession< const handoffTools = handoffs.map((handoff) => handoff.getHandoffAsFunctionTool(), ); + const allTools = await ( + this.#currentAgent as RealtimeAgent + ).getAllTools(this.#context); this.#currentTools = [ - ...(await this.#currentAgent.getAllTools(this.#context)).filter( - (tool) => tool.type === 'function', - ), + ...allTools.filter((tool) => tool.type === 'function'), ...handoffTools, ]; } @@ -444,12 +445,10 @@ export class RealtimeSession< .map((handoff) => [handoff.toolName, handoff]), ); - const functionToolMap = new Map( - (await this.#currentAgent.getAllTools(this.#context)).map((tool) => [ - tool.name, - tool, - ]), - ); + const allTools = await ( + this.#currentAgent as RealtimeAgent + ).getAllTools(this.#context); + const functionToolMap = new Map(allTools.map((tool) => [tool.name, tool])); const possibleHandoff = handoffMap.get(toolCall.name); if (possibleHandoff) { From ac72c06f1415f633b9d058dd5d7fe215eb72e6df Mon Sep 17 00:00:00 2001 From: Viraj <123119434+vrtnis@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:56:00 -0700 Subject: [PATCH 03/14] feat(examples): add MCP tool-filter example --- examples/mcp/README.md | 6 ++++ examples/mcp/package.json | 3 +- examples/mcp/tool-filter-example.ts | 51 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 examples/mcp/tool-filter-example.ts diff --git a/examples/mcp/README.md b/examples/mcp/README.md index c7d31256..858ed2a3 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -12,3 +12,9 @@ Run the example from the repository root: ```bash pnpm -F mcp start:stdio ``` + +`tool-filter-example.ts` shows how to expose only a subset of server tools: + +```bash +pnpm -F mcp start:tool-filter +``` diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 759130ab..c1632839 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -12,6 +12,7 @@ "start:streamable-http": "tsx streamable-http-example.ts", "start:hosted-mcp-on-approval": "tsx hosted-mcp-on-approval.ts", "start:hosted-mcp-human-in-the-loop": "tsx hosted-mcp-human-in-the-loop.ts", - "start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts" + "start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts", + "start:tool-filter": "tsx tool-filter-example.ts" } } diff --git a/examples/mcp/tool-filter-example.ts b/examples/mcp/tool-filter-example.ts new file mode 100644 index 00000000..5c46ba20 --- /dev/null +++ b/examples/mcp/tool-filter-example.ts @@ -0,0 +1,51 @@ +import { + Agent, + run, + MCPServerStdio, + createStaticToolFilter, + withTrace, +} from '@openai/agents'; +import * as path from 'node:path'; + +async function main() { + const samplesDir = path.join(__dirname, 'sample_files'); + const mcpServer = new MCPServerStdio({ + name: 'Filesystem Server with filter', + fullCommand: `npx -y @modelcontextprotocol/server-filesystem ${samplesDir}`, + toolFilter: createStaticToolFilter( + ['read_file', 'list_directory'], + ['write_file'], + ), + }); + + await mcpServer.connect(); + + try { + await withTrace('MCP Tool Filter Example', async () => { + const agent = new Agent({ + name: 'MCP Assistant', + instructions: + 'Use the filesystem tools to answer questions. The write_file tool is blocked via toolFilter.', + mcpServers: [mcpServer], + }); + + console.log('Listing sample files:'); + let result = await run(agent, 'List the files in the current directory.'); + console.log(result.finalOutput); + + console.log('\nAttempting to write a file (should be blocked):'); + result = await run( + agent, + 'Create a file named test.txt with the text "hello"', + ); + console.log(result.finalOutput); + }); + } finally { + await mcpServer.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From 612c24e88604a25815da474dfe6456cf5313e7df Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:43:58 -0700 Subject: [PATCH 04/14] chore: add changeset for MCP tool-filtering support (fixes #162) --- .changeset/hungry-suns-search.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/hungry-suns-search.md diff --git a/.changeset/hungry-suns-search.md b/.changeset/hungry-suns-search.md new file mode 100644 index 00000000..8f2b4433 --- /dev/null +++ b/.changeset/hungry-suns-search.md @@ -0,0 +1,6 @@ +--- +'@openai/agents-realtime': minor +'@openai/agents-core': minor +--- + +agents-core, agents-realtime: add MCP tool-filtering support (fixes #162) From 3161fd7f637c37242f6696d690b5d39762736803 Mon Sep 17 00:00:00 2001 From: Viraj <123119434+vrtnis@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:57:31 -0700 Subject: [PATCH 05/14] Update .changeset/hungry-suns-search.md Co-authored-by: Kazuhiro Sera --- .changeset/hungry-suns-search.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/hungry-suns-search.md b/.changeset/hungry-suns-search.md index 8f2b4433..56b112c1 100644 --- a/.changeset/hungry-suns-search.md +++ b/.changeset/hungry-suns-search.md @@ -1,6 +1,6 @@ --- -'@openai/agents-realtime': minor -'@openai/agents-core': minor +'@openai/agents-realtime': patch +'@openai/agents-core': patch --- agents-core, agents-realtime: add MCP tool-filtering support (fixes #162) From cc377bc18f1cd8ef148051b9b59f325b7ee7d685 Mon Sep 17 00:00:00 2001 From: Viraj <123119434+vrtnis@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:58:15 -0700 Subject: [PATCH 06/14] Update docs/src/content/docs/guides/mcp.mdx Co-authored-by: Kazuhiro Sera --- docs/src/content/docs/guides/mcp.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/guides/mcp.mdx b/docs/src/content/docs/guides/mcp.mdx index 66a1d5a8..d02a7614 100644 --- a/docs/src/content/docs/guides/mcp.mdx +++ b/docs/src/content/docs/guides/mcp.mdx @@ -105,7 +105,7 @@ using `createStaticToolFilter` or a custom function: ```ts const server = new MCPServerStdio({ fullCommand: 'my-server', - toolFilter: createStaticToolFilter(['safe_tool'], ['danger_tool']), + toolFilter: createStaticToolFilter({ allowed: ['safe_tool'], blocked: ['danger_tool'] }), }); const dynamicServer = new MCPServerStreamableHttp({ From ce756c2d4b796c45fe928751b0a236ec36d8277e Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:39:54 -0700 Subject: [PATCH 07/14] refactor: integrate filtered tool logic into agent-level, clean up tests and server impl --- examples/mcp/tool-filter-example.ts | 8 +- packages/agents-core/src/mcpUtil.ts | 18 +-- .../agents-core/src/shims/mcp-server/node.ts | 126 +++--------------- .../agents-core/test/mcpToolFilter.test.ts | 16 +-- 4 files changed, 39 insertions(+), 129 deletions(-) diff --git a/examples/mcp/tool-filter-example.ts b/examples/mcp/tool-filter-example.ts index 5c46ba20..6bae8c96 100644 --- a/examples/mcp/tool-filter-example.ts +++ b/examples/mcp/tool-filter-example.ts @@ -12,10 +12,10 @@ async function main() { const mcpServer = new MCPServerStdio({ name: 'Filesystem Server with filter', fullCommand: `npx -y @modelcontextprotocol/server-filesystem ${samplesDir}`, - toolFilter: createStaticToolFilter( - ['read_file', 'list_directory'], - ['write_file'], - ), + toolFilter: createStaticToolFilter({ + allowed: ['read_file', 'list_directory'], + blocked: ['write_file'], + }), }); await mcpServer.connect(); diff --git a/packages/agents-core/src/mcpUtil.ts b/packages/agents-core/src/mcpUtil.ts index 8496043f..0a3a0724 100644 --- a/packages/agents-core/src/mcpUtil.ts +++ b/packages/agents-core/src/mcpUtil.ts @@ -28,19 +28,19 @@ export interface ToolFilterStatic { } /** Convenience helper to create a static tool filter. */ -export function createStaticToolFilter( - allowedToolNames?: string[], - blockedToolNames?: string[], -): ToolFilterStatic | undefined { - if (!allowedToolNames && !blockedToolNames) { +export function createStaticToolFilter(options?: { + allowed?: string[]; + blocked?: string[]; +}): ToolFilterStatic | undefined { + if (!options?.allowed && !options?.blocked) { return undefined; } const filter: ToolFilterStatic = {}; - if (allowedToolNames) { - filter.allowedToolNames = allowedToolNames; + if (options?.allowed) { + filter.allowedToolNames = options.allowed; } - if (blockedToolNames) { - filter.blockedToolNames = blockedToolNames; + if (options?.blocked) { + filter.blockedToolNames = options.blocked; } return filter; } diff --git a/packages/agents-core/src/shims/mcp-server/node.ts b/packages/agents-core/src/shims/mcp-server/node.ts index 00752c80..7a7825a7 100644 --- a/packages/agents-core/src/shims/mcp-server/node.ts +++ b/packages/agents-core/src/shims/mcp-server/node.ts @@ -12,7 +12,6 @@ import { invalidateServerToolsCache, } from '../../mcp'; import logger from '../../logger'; -import type { ToolFilterContext } from '../../mcpUtil'; import type { RunContext } from '../../runContext'; import type { Agent } from '../../agent'; @@ -99,52 +98,9 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio { this._cacheDirty = true; } - // The response element type is intentionally left as `any` to avoid explosing MCP SDK type dependencies. - protected async _applyToolFilter( - tools: MCPTool[], - runContext?: RunContext, - agent?: Agent, - ): Promise { - if (!this.toolFilter) { - return tools; - } - - if (typeof this.toolFilter === 'function') { - const ctx = { - runContext: runContext as RunContext, - agent: agent as Agent, - serverName: this.name, - } as ToolFilterContext; - const filtered: MCPTool[] = []; - for (const t of tools) { - try { - const res = this.toolFilter(ctx, t); - const include = res instanceof Promise ? await res : res; - if (include) filtered.push(t); - } catch (e) { - this.logger.error( - `Error applying tool filter to tool '${t.name}' on server '${this.name}': ${e}`, - ); - } - } - return filtered; - } - - let filtered = tools; - if (this.toolFilter.allowedToolNames) { - const allowed = new Set(this.toolFilter.allowedToolNames); - filtered = filtered.filter((t) => allowed.has(t.name)); - } - if (this.toolFilter.blockedToolNames) { - const blocked = new Set(this.toolFilter.blockedToolNames); - filtered = filtered.filter((t) => !blocked.has(t.name)); - } - return filtered; - } - async listTools( - runContext?: RunContext, - agent?: Agent, + _runContext?: RunContext, + _agent?: Agent, ): Promise { const { ListToolsResultSchema } = await import( '@modelcontextprotocol/sdk/types.js' @@ -154,17 +110,15 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio { 'Server not initialized. Make sure you call connect() first.', ); } - let tools: MCPTool[]; if (this.cacheToolsList && !this._cacheDirty && this._toolsList) { - tools = this._toolsList; - } else { - this._cacheDirty = false; - const response = await this.session.listTools(); - this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); - this._toolsList = ListToolsResultSchema.parse(response).tools; - tools = this._toolsList; + return this._toolsList; } - return this._applyToolFilter(tools, runContext, agent); + + this._cacheDirty = false; + const response = await this.session.listTools(); + this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); + this._toolsList = ListToolsResultSchema.parse(response).tools; + return this._toolsList; } async callTool( @@ -264,51 +218,9 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp { this._cacheDirty = true; } - // The response element type is intentionally left as `any` to avoid explosing MCP SDK type dependencies. - protected async _applyToolFilter( - tools: MCPTool[], - runContext?: RunContext, - agent?: Agent, - ): Promise { - if (!this.toolFilter) { - return tools; - } - if (typeof this.toolFilter === 'function') { - const ctx: ToolFilterContext = { - runContext: runContext as RunContext, - agent: agent as Agent, - serverName: this.name, - }; - const filtered: MCPTool[] = []; - for (const t of tools) { - try { - const res = this.toolFilter(ctx, t); - const include = res instanceof Promise ? await res : res; - if (include) filtered.push(t); - } catch (e) { - this.logger.error( - `Error applying tool filter to tool '${t.name}' on server '${this.name}': ${e}`, - ); - } - } - return filtered; - } - - let filtered = tools; - if (this.toolFilter.allowedToolNames) { - const allowed = new Set(this.toolFilter.allowedToolNames); - filtered = filtered.filter((t) => allowed.has(t.name)); - } - if (this.toolFilter.blockedToolNames) { - const blocked = new Set(this.toolFilter.blockedToolNames); - filtered = filtered.filter((t) => !blocked.has(t.name)); - } - return filtered; - } - async listTools( - runContext?: RunContext, - agent?: Agent, + _runContext?: RunContext, + _agent?: Agent, ): Promise { const { ListToolsResultSchema } = await import( '@modelcontextprotocol/sdk/types.js' @@ -318,17 +230,15 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp { 'Server not initialized. Make sure you call connect() first.', ); } - let tools: MCPTool[]; if (this.cacheToolsList && !this._cacheDirty && this._toolsList) { - tools = this._toolsList; - } else { - this._cacheDirty = false; - const response = await this.session.listTools(); - this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); - this._toolsList = ListToolsResultSchema.parse(response).tools; - tools = this._toolsList; + return this._toolsList; } - return this._applyToolFilter(tools, runContext, agent); + + this._cacheDirty = false; + const response = await this.session.listTools(); + this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); + this._toolsList = ListToolsResultSchema.parse(response).tools; + return this._toolsList; } async callTool( diff --git a/packages/agents-core/test/mcpToolFilter.test.ts b/packages/agents-core/test/mcpToolFilter.test.ts index 6823a274..54346012 100644 --- a/packages/agents-core/test/mcpToolFilter.test.ts +++ b/packages/agents-core/test/mcpToolFilter.test.ts @@ -49,7 +49,7 @@ describe('MCP tool filtering', () => { const server = new StubServer( 's', tools, - createStaticToolFilter(['a'], ['b']), + createStaticToolFilter({ allowed: ['a'], blocked: ['b'] }), ); const agent = new Agent({ name: 'agent', @@ -61,7 +61,7 @@ describe('MCP tool filtering', () => { }); const runContext = new RunContext(); const result = await server.listTools(runContext, agent); - expect(result.map((t) => t.name)).toEqual(['a']); + expect(result.map((t) => t.name)).toEqual(['a', 'b']); }); }); @@ -101,7 +101,7 @@ describe('MCP tool filtering', () => { }); const runContext = new RunContext(); const result = await server.listTools(runContext, agent); - expect(result.map((t) => t.name)).toEqual(['good']); + expect(result.map((t) => t.name)).toEqual(['good', 'bad']); }); }); @@ -144,7 +144,7 @@ describe('MCP tool filtering', () => { const serverA = new StubServer( 'A', toolsA, - createStaticToolFilter(['a1']), + createStaticToolFilter({ allowed: ['a1'] }), ); const serverB = new StubServer('B', toolsB); const agent = new Agent({ @@ -158,7 +158,7 @@ describe('MCP tool filtering', () => { const runContext = new RunContext(); const resultA = await serverA.listTools(runContext, agent); const resultB = await serverB.listTools(runContext, agent); - expect(resultA.map((t) => t.name)).toEqual(['a1']); + expect(resultA.map((t) => t.name)).toEqual(['a1', 'a2']); expect(resultB.map((t) => t.name)).toEqual(['b1']); }); }); @@ -180,7 +180,7 @@ describe('MCP tool filtering', () => { const server = new StubServer( 'cache', tools, - createStaticToolFilter(['x']), + createStaticToolFilter({ allowed: ['x'] }), ); const agent = new Agent({ name: 'agent', @@ -193,9 +193,9 @@ describe('MCP tool filtering', () => { const runContext = new RunContext(); let result = await server.listTools(runContext, agent); expect(result.map((t) => t.name)).toEqual(['x']); - server.toolFilter = createStaticToolFilter(['y']); + (server as any).toolFilter = createStaticToolFilter({ allowed: ['y'] }); result = await server.listTools(runContext, agent); - expect(result.map((t) => t.name)).toEqual([]); + expect(result.map((t) => t.name)).toEqual(['x']); }); }); }); From 7456bbfaf07ddea8c034c8a049422f07c8e7f9ca Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:18:41 -0700 Subject: [PATCH 08/14] refactor(core): update MCP tool filtering implementation - Moved tool filtering logic to agent/runner layer - Removed server-side filtering and context coupling - Updated test suite to reflect new behavior --- packages/agents-core/src/mcp.ts | 69 ++++++++++++++++--- .../agents-core/test/mcpToolFilter.test.ts | 44 ++++++++++++ 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index 60c74cc0..a2da3f10 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -14,7 +14,11 @@ import { JsonObjectSchemaStrict, UnknownContext, } from './types'; -import type { ToolFilterCallable, ToolFilterStatic } from './mcpUtil'; +import type { + ToolFilterCallable, + ToolFilterStatic, + ToolFilterContext, +} from './mcpUtil'; import type { RunContext } from './runContext'; import type { Agent } from './agent'; @@ -30,6 +34,7 @@ export const DEFAULT_STREAMABLE_HTTP_MCP_CLIENT_LOGGER_NAME = */ export interface MCPServer { cacheToolsList: boolean; + toolFilter?: ToolFilterCallable | ToolFilterStatic; connect(): Promise; readonly name: string; close(): Promise; @@ -46,7 +51,7 @@ export interface MCPServer { export abstract class BaseMCPServerStdio implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - protected toolFilter?: ToolFilterCallable | ToolFilterStatic; + public toolFilter?: ToolFilterCallable | ToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStdioOptions) { @@ -83,7 +88,7 @@ export abstract class BaseMCPServerStdio implements MCPServer { export abstract class BaseMCPServerStreamableHttp implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - protected toolFilter?: ToolFilterCallable | ToolFilterStatic; + public toolFilter?: ToolFilterCallable | ToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStreamableHttpOptions) { @@ -155,13 +160,13 @@ export class MCPServerStdio extends BaseMCPServerStdio { return this.underlying.close(); } async listTools( - runContext?: RunContext, - agent?: Agent, + _runContext?: RunContext, + _agent?: Agent, ): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(runContext, agent); + const tools = await this.underlying.listTools(); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -191,13 +196,13 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { return this.underlying.close(); } async listTools( - runContext?: RunContext, - agent?: Agent, + _runContext?: RunContext, + _agent?: Agent, ): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(runContext, agent); + const tools = await this.underlying.listTools(); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -268,7 +273,16 @@ async function getFunctionToolsFromServer( } return withMCPListToolsSpan( async (span) => { - const mcpTools = await server.listTools(runContext, agent); + let mcpTools = await server.listTools(runContext, agent); + if (server.toolFilter) { + mcpTools = await filterMcpTools( + mcpTools, + server.toolFilter as ToolFilterCallable | ToolFilterStatic, + runContext, + agent, + server.name, + ); + } span.spanData.result = mcpTools.map((t) => t.name); const tools: FunctionTool[] = mcpTools.map((t) => mcpToFunctionTool(t, server, convertSchemasToStrict), @@ -371,6 +385,41 @@ function ensureStrictJsonSchema( return out; } +async function filterMcpTools( + tools: MCPTool[], + filter: ToolFilterCallable | ToolFilterStatic, + runContext: RunContext | undefined, + agent: Agent | undefined, + serverName: string, +): Promise { + if (typeof filter === 'function') { + if (!runContext || !agent) { + return tools; + } + const ctx = { + runContext, + agent, + serverName, + } as ToolFilterContext; + const result: MCPTool[] = []; + for (const tool of tools) { + if (await filter(ctx, tool)) { + result.push(tool); + } + } + return result; + } + return tools.filter((t) => { + if (filter.allowedToolNames && !filter.allowedToolNames.includes(t.name)) { + return false; + } + if (filter.blockedToolNames && filter.blockedToolNames.includes(t.name)) { + return false; + } + return true; + }); +} + /** * Abstract base class for MCP servers that use a ClientSession for communication. * Handles session management, tool listing, tool calling, and cleanup. diff --git a/packages/agents-core/test/mcpToolFilter.test.ts b/packages/agents-core/test/mcpToolFilter.test.ts index 54346012..bb3a0f2c 100644 --- a/packages/agents-core/test/mcpToolFilter.test.ts +++ b/packages/agents-core/test/mcpToolFilter.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { withTrace } from '../src/tracing'; import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; import { createStaticToolFilter } from '../src/mcpUtil'; +import { getAllMcpTools } from '../src/mcp'; import { Agent } from '../src/agent'; import { RunContext } from '../src/runContext'; @@ -198,4 +199,47 @@ describe('MCP tool filtering', () => { expect(result.map((t) => t.name)).toEqual(['x']); }); }); + + it('applies filter in getAllMcpTools', async () => { + await withTrace('test', async () => { + const tools = [ + { + name: 'allow', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'block', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + const server = new StubServer( + 'filter', + tools, + createStaticToolFilter({ allowed: ['allow'] }), + ); + const agent = new Agent({ + name: 'agent', + instructions: '', + model: '', + modelSettings: {}, + tools: [], + mcpServers: [server], + }); + const runContext = new RunContext(); + const result = await getAllMcpTools([server], false, runContext, agent); + expect(result.map((t) => t.name)).toEqual(['allow']); + }); + }); }); From 47ae3474b746ac4067b134f597bc67c6a606e2d5 Mon Sep 17 00:00:00 2001 From: Viraj <123119434+vrtnis@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:52:01 -0700 Subject: [PATCH 09/14] revert: restore MCPServer interface and listTools signature Reverts prior tool filtering interface changes and updates corresponding tests. --- packages/agents-core/src/mcp.ts | 69 +++---------------- .../agents-core/test/mcpToolFilter.test.ts | 44 ------------ 2 files changed, 10 insertions(+), 103 deletions(-) diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index a2da3f10..60c74cc0 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -14,11 +14,7 @@ import { JsonObjectSchemaStrict, UnknownContext, } from './types'; -import type { - ToolFilterCallable, - ToolFilterStatic, - ToolFilterContext, -} from './mcpUtil'; +import type { ToolFilterCallable, ToolFilterStatic } from './mcpUtil'; import type { RunContext } from './runContext'; import type { Agent } from './agent'; @@ -34,7 +30,6 @@ export const DEFAULT_STREAMABLE_HTTP_MCP_CLIENT_LOGGER_NAME = */ export interface MCPServer { cacheToolsList: boolean; - toolFilter?: ToolFilterCallable | ToolFilterStatic; connect(): Promise; readonly name: string; close(): Promise; @@ -51,7 +46,7 @@ export interface MCPServer { export abstract class BaseMCPServerStdio implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - public toolFilter?: ToolFilterCallable | ToolFilterStatic; + protected toolFilter?: ToolFilterCallable | ToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStdioOptions) { @@ -88,7 +83,7 @@ export abstract class BaseMCPServerStdio implements MCPServer { export abstract class BaseMCPServerStreamableHttp implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - public toolFilter?: ToolFilterCallable | ToolFilterStatic; + protected toolFilter?: ToolFilterCallable | ToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStreamableHttpOptions) { @@ -160,13 +155,13 @@ export class MCPServerStdio extends BaseMCPServerStdio { return this.underlying.close(); } async listTools( - _runContext?: RunContext, - _agent?: Agent, + runContext?: RunContext, + agent?: Agent, ): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(); + const tools = await this.underlying.listTools(runContext, agent); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -196,13 +191,13 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { return this.underlying.close(); } async listTools( - _runContext?: RunContext, - _agent?: Agent, + runContext?: RunContext, + agent?: Agent, ): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(); + const tools = await this.underlying.listTools(runContext, agent); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -273,16 +268,7 @@ async function getFunctionToolsFromServer( } return withMCPListToolsSpan( async (span) => { - let mcpTools = await server.listTools(runContext, agent); - if (server.toolFilter) { - mcpTools = await filterMcpTools( - mcpTools, - server.toolFilter as ToolFilterCallable | ToolFilterStatic, - runContext, - agent, - server.name, - ); - } + const mcpTools = await server.listTools(runContext, agent); span.spanData.result = mcpTools.map((t) => t.name); const tools: FunctionTool[] = mcpTools.map((t) => mcpToFunctionTool(t, server, convertSchemasToStrict), @@ -385,41 +371,6 @@ function ensureStrictJsonSchema( return out; } -async function filterMcpTools( - tools: MCPTool[], - filter: ToolFilterCallable | ToolFilterStatic, - runContext: RunContext | undefined, - agent: Agent | undefined, - serverName: string, -): Promise { - if (typeof filter === 'function') { - if (!runContext || !agent) { - return tools; - } - const ctx = { - runContext, - agent, - serverName, - } as ToolFilterContext; - const result: MCPTool[] = []; - for (const tool of tools) { - if (await filter(ctx, tool)) { - result.push(tool); - } - } - return result; - } - return tools.filter((t) => { - if (filter.allowedToolNames && !filter.allowedToolNames.includes(t.name)) { - return false; - } - if (filter.blockedToolNames && filter.blockedToolNames.includes(t.name)) { - return false; - } - return true; - }); -} - /** * Abstract base class for MCP servers that use a ClientSession for communication. * Handles session management, tool listing, tool calling, and cleanup. diff --git a/packages/agents-core/test/mcpToolFilter.test.ts b/packages/agents-core/test/mcpToolFilter.test.ts index bb3a0f2c..54346012 100644 --- a/packages/agents-core/test/mcpToolFilter.test.ts +++ b/packages/agents-core/test/mcpToolFilter.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest'; import { withTrace } from '../src/tracing'; import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; import { createStaticToolFilter } from '../src/mcpUtil'; -import { getAllMcpTools } from '../src/mcp'; import { Agent } from '../src/agent'; import { RunContext } from '../src/runContext'; @@ -199,47 +198,4 @@ describe('MCP tool filtering', () => { expect(result.map((t) => t.name)).toEqual(['x']); }); }); - - it('applies filter in getAllMcpTools', async () => { - await withTrace('test', async () => { - const tools = [ - { - name: 'allow', - description: '', - inputSchema: { - type: 'object', - properties: {}, - required: [], - additionalProperties: false, - }, - }, - { - name: 'block', - description: '', - inputSchema: { - type: 'object', - properties: {}, - required: [], - additionalProperties: false, - }, - }, - ]; - const server = new StubServer( - 'filter', - tools, - createStaticToolFilter({ allowed: ['allow'] }), - ); - const agent = new Agent({ - name: 'agent', - instructions: '', - model: '', - modelSettings: {}, - tools: [], - mcpServers: [server], - }); - const runContext = new RunContext(); - const result = await getAllMcpTools([server], false, runContext, agent); - expect(result.map((t) => t.name)).toEqual(['allow']); - }); - }); }); From 4c1adbd3dc7631ab01b404ff9b7aa4cdb5ead6b4 Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:18:06 -0700 Subject: [PATCH 10/14] refactor(core): prefix tool filter names with MCP --- docs/src/content/docs/guides/mcp.mdx | 10 +++++++--- examples/mcp/tool-filter-example.ts | 4 ++-- packages/agents-core/src/index.ts | 8 ++++---- packages/agents-core/src/mcp.ts | 10 +++++----- packages/agents-core/src/mcpUtil.ts | 14 +++++++------- packages/agents-core/test/mcpToolFilter.test.ts | 12 +++++++----- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/src/content/docs/guides/mcp.mdx b/docs/src/content/docs/guides/mcp.mdx index d02a7614..115e50aa 100644 --- a/docs/src/content/docs/guides/mcp.mdx +++ b/docs/src/content/docs/guides/mcp.mdx @@ -100,17 +100,21 @@ Only enable this if you're confident the tool list won't change. To invalidate t ### Tool filtering You can restrict which tools are exposed from each server. Pass either a static filter -using `createStaticToolFilter` or a custom function: +using `createMCPToolStaticFilter` or a custom function: ```ts const server = new MCPServerStdio({ fullCommand: 'my-server', - toolFilter: createStaticToolFilter({ allowed: ['safe_tool'], blocked: ['danger_tool'] }), + toolFilter: createMCPToolStaticFilter({ + allowed: ['safe_tool'], + blocked: ['danger_tool'], + }), }); const dynamicServer = new MCPServerStreamableHttp({ url: 'http://localhost:3000', - toolFilter: ({ runContext }, tool) => runContext.context.allowAll || tool.name !== 'admin', + toolFilter: ({ runContext }, tool) => + runContext.context.allowAll || tool.name !== 'admin', }); ``` diff --git a/examples/mcp/tool-filter-example.ts b/examples/mcp/tool-filter-example.ts index 6bae8c96..0c6bae60 100644 --- a/examples/mcp/tool-filter-example.ts +++ b/examples/mcp/tool-filter-example.ts @@ -2,7 +2,7 @@ import { Agent, run, MCPServerStdio, - createStaticToolFilter, + createMCPToolStaticFilter, withTrace, } from '@openai/agents'; import * as path from 'node:path'; @@ -12,7 +12,7 @@ async function main() { const mcpServer = new MCPServerStdio({ name: 'Filesystem Server with filter', fullCommand: `npx -y @modelcontextprotocol/server-filesystem ${samplesDir}`, - toolFilter: createStaticToolFilter({ + toolFilter: createMCPToolStaticFilter({ allowed: ['read_file', 'list_directory'], blocked: ['write_file'], }), diff --git a/packages/agents-core/src/index.ts b/packages/agents-core/src/index.ts index e5d689b1..6a359cc4 100644 --- a/packages/agents-core/src/index.ts +++ b/packages/agents-core/src/index.ts @@ -74,10 +74,10 @@ export { MCPServerStreamableHttp, } from './mcp'; export { - ToolFilterCallable, - ToolFilterContext, - ToolFilterStatic, - createStaticToolFilter, + MCPToolFilterCallable, + MCPToolFilterContext, + MCPToolFilterStatic, + createMCPToolStaticFilter, } from './mcpUtil'; export { Model, diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index 60c74cc0..b9dece42 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -14,7 +14,7 @@ import { JsonObjectSchemaStrict, UnknownContext, } from './types'; -import type { ToolFilterCallable, ToolFilterStatic } from './mcpUtil'; +import type { MCPToolFilterCallable, MCPToolFilterStatic } from './mcpUtil'; import type { RunContext } from './runContext'; import type { Agent } from './agent'; @@ -46,7 +46,7 @@ export interface MCPServer { export abstract class BaseMCPServerStdio implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - protected toolFilter?: ToolFilterCallable | ToolFilterStatic; + protected toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStdioOptions) { @@ -83,7 +83,7 @@ export abstract class BaseMCPServerStdio implements MCPServer { export abstract class BaseMCPServerStreamableHttp implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - protected toolFilter?: ToolFilterCallable | ToolFilterStatic; + protected toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStreamableHttpOptions) { @@ -386,7 +386,7 @@ export interface BaseMCPServerStdioOptions { encoding?: string; encodingErrorHandler?: 'strict' | 'ignore' | 'replace'; logger?: Logger; - toolFilter?: ToolFilterCallable | ToolFilterStatic; + toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; } export interface DefaultMCPServerStdioOptions extends BaseMCPServerStdioOptions { @@ -407,7 +407,7 @@ export interface MCPServerStreamableHttpOptions { clientSessionTimeoutSeconds?: number; name?: string; logger?: Logger; - toolFilter?: ToolFilterCallable | ToolFilterStatic; + toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; // ---------------------------------------------------- // OAuth diff --git a/packages/agents-core/src/mcpUtil.ts b/packages/agents-core/src/mcpUtil.ts index 0a3a0724..5d90ab10 100644 --- a/packages/agents-core/src/mcpUtil.ts +++ b/packages/agents-core/src/mcpUtil.ts @@ -4,7 +4,7 @@ import type { MCPTool } from './mcp'; import type { UnknownContext } from './types'; /** Context information available to tool filter functions. */ -export interface ToolFilterContext { +export interface MCPToolFilterContext { /** The current run context. */ runContext: RunContext; /** The agent requesting the tools. */ @@ -14,13 +14,13 @@ export interface ToolFilterContext { } /** A function that determines whether a tool should be available. */ -export type ToolFilterCallable = ( - context: ToolFilterContext, +export type MCPToolFilterCallable = ( + context: MCPToolFilterContext, tool: MCPTool, ) => boolean | Promise; /** Static tool filter configuration using allow and block lists. */ -export interface ToolFilterStatic { +export interface MCPToolFilterStatic { /** Optional list of tool names to allow. */ allowedToolNames?: string[]; /** Optional list of tool names to block. */ @@ -28,14 +28,14 @@ export interface ToolFilterStatic { } /** Convenience helper to create a static tool filter. */ -export function createStaticToolFilter(options?: { +export function createMCPToolStaticFilter(options?: { allowed?: string[]; blocked?: string[]; -}): ToolFilterStatic | undefined { +}): MCPToolFilterStatic | undefined { if (!options?.allowed && !options?.blocked) { return undefined; } - const filter: ToolFilterStatic = {}; + const filter: MCPToolFilterStatic = {}; if (options?.allowed) { filter.allowedToolNames = options.allowed; } diff --git a/packages/agents-core/test/mcpToolFilter.test.ts b/packages/agents-core/test/mcpToolFilter.test.ts index 54346012..23a0e7de 100644 --- a/packages/agents-core/test/mcpToolFilter.test.ts +++ b/packages/agents-core/test/mcpToolFilter.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { withTrace } from '../src/tracing'; import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; -import { createStaticToolFilter } from '../src/mcpUtil'; +import { createMCPToolStaticFilter } from '../src/mcpUtil'; import { Agent } from '../src/agent'; import { RunContext } from '../src/runContext'; @@ -49,7 +49,7 @@ describe('MCP tool filtering', () => { const server = new StubServer( 's', tools, - createStaticToolFilter({ allowed: ['a'], blocked: ['b'] }), + createMCPToolStaticFilter({ allowed: ['a'], blocked: ['b'] }), ); const agent = new Agent({ name: 'agent', @@ -144,7 +144,7 @@ describe('MCP tool filtering', () => { const serverA = new StubServer( 'A', toolsA, - createStaticToolFilter({ allowed: ['a1'] }), + createMCPToolStaticFilter({ allowed: ['a1'] }), ); const serverB = new StubServer('B', toolsB); const agent = new Agent({ @@ -180,7 +180,7 @@ describe('MCP tool filtering', () => { const server = new StubServer( 'cache', tools, - createStaticToolFilter({ allowed: ['x'] }), + createMCPToolStaticFilter({ allowed: ['x'] }), ); const agent = new Agent({ name: 'agent', @@ -193,7 +193,9 @@ describe('MCP tool filtering', () => { const runContext = new RunContext(); let result = await server.listTools(runContext, agent); expect(result.map((t) => t.name)).toEqual(['x']); - (server as any).toolFilter = createStaticToolFilter({ allowed: ['y'] }); + (server as any).toolFilter = createMCPToolStaticFilter({ + allowed: ['y'], + }); result = await server.listTools(runContext, agent); expect(result.map((t) => t.name)).toEqual(['x']); }); From dc5bc155b30f4dea1df7b927cc1ad1957f15307c Mon Sep 17 00:00:00 2001 From: gilbertl Date: Wed, 16 Jul 2025 01:50:01 +0800 Subject: [PATCH 11/14] Handle function call messages with empty content (#203) * Handle function call messages with empty content * Resolve test failure * Create heavy-yaks-mate.md --------- Co-authored-by: Dominik Kundel --- .changeset/heavy-yaks-mate.md | 5 +++++ packages/agents-openai/src/openaiChatCompletionsModel.ts | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/heavy-yaks-mate.md diff --git a/.changeset/heavy-yaks-mate.md b/.changeset/heavy-yaks-mate.md new file mode 100644 index 00000000..3094f31d --- /dev/null +++ b/.changeset/heavy-yaks-mate.md @@ -0,0 +1,5 @@ +--- +"@openai/agents-openai": patch +--- + +Handle function call messages with empty content in Chat Completions diff --git a/packages/agents-openai/src/openaiChatCompletionsModel.ts b/packages/agents-openai/src/openaiChatCompletionsModel.ts index 885eadaa..e4829f84 100644 --- a/packages/agents-openai/src/openaiChatCompletionsModel.ts +++ b/packages/agents-openai/src/openaiChatCompletionsModel.ts @@ -67,7 +67,13 @@ export class OpenAIChatCompletionsModel implements Model { const output: protocol.OutputModelItem[] = []; if (response.choices && response.choices[0]) { const message = response.choices[0].message; - if (message.content !== undefined && message.content !== null) { + + if ( + message.content !== undefined && + message.content !== null && + // Azure OpenAI returns empty string instead of null for tool calls, causing parser rejection + !(message.tool_calls && message.content === '') + ) { const { content, ...rest } = message; output.push({ id: response.id, From 73eb37ae81801f9f64c4853e98338c7404a195ff Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 16 Jul 2025 15:53:16 +0900 Subject: [PATCH 12/14] wip --- examples/mcp/tool-filter-example.ts | 5 +- packages/agents-core/src/agent.ts | 2 +- packages/agents-core/src/mcp.ts | 98 ++++++++++++------- packages/agents-core/src/mcpUtil.ts | 2 +- packages/agents-core/src/runState.ts | 6 +- .../src/shims/mcp-server/browser.ts | 12 +-- .../agents-core/src/shims/mcp-server/node.ts | 12 +-- packages/agents-core/test/mcpCache.test.ts | 20 +++- .../agents-core/test/mcpToolFilter.test.ts | 50 ++-------- 9 files changed, 99 insertions(+), 108 deletions(-) diff --git a/examples/mcp/tool-filter-example.ts b/examples/mcp/tool-filter-example.ts index 0c6bae60..5e5c8d6a 100644 --- a/examples/mcp/tool-filter-example.ts +++ b/examples/mcp/tool-filter-example.ts @@ -24,8 +24,7 @@ async function main() { await withTrace('MCP Tool Filter Example', async () => { const agent = new Agent({ name: 'MCP Assistant', - instructions: - 'Use the filesystem tools to answer questions. The write_file tool is blocked via toolFilter.', + instructions: 'Use the filesystem tools to answer questions.', mcpServers: [mcpServer], }); @@ -36,7 +35,7 @@ async function main() { console.log('\nAttempting to write a file (should be blocked):'); result = await run( agent, - 'Create a file named test.txt with the text "hello"', + 'Create a file named sample_files/test.txt with the text "hello"', ); console.log(result.finalOutput); }); diff --git a/packages/agents-core/src/agent.ts b/packages/agents-core/src/agent.ts index e5118bbf..27d00090 100644 --- a/packages/agents-core/src/agent.ts +++ b/packages/agents-core/src/agent.ts @@ -518,7 +518,7 @@ export class Agent< runContext: RunContext, ): Promise[]> { if (this.mcpServers.length > 0) { - return getAllMcpTools(this.mcpServers, false, runContext, this); + return getAllMcpTools(this.mcpServers, runContext, this, false); } return []; diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index b9dece42..08c35fb3 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -30,13 +30,11 @@ export const DEFAULT_STREAMABLE_HTTP_MCP_CLIENT_LOGGER_NAME = */ export interface MCPServer { cacheToolsList: boolean; + toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; connect(): Promise; readonly name: string; close(): Promise; - listTools( - runContext?: RunContext, - agent?: Agent, - ): Promise; + listTools(): Promise; callTool( toolName: string, args: Record | null, @@ -46,7 +44,7 @@ export interface MCPServer { export abstract class BaseMCPServerStdio implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - protected toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; + public toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStdioOptions) { @@ -59,10 +57,7 @@ export abstract class BaseMCPServerStdio implements MCPServer { abstract get name(): string; abstract connect(): Promise; abstract close(): Promise; - abstract listTools( - runContext?: RunContext, - agent?: Agent, - ): Promise; + abstract listTools(): Promise; abstract callTool( _toolName: string, _args: Record | null, @@ -83,7 +78,7 @@ export abstract class BaseMCPServerStdio implements MCPServer { export abstract class BaseMCPServerStreamableHttp implements MCPServer { public cacheToolsList: boolean; protected _cachedTools: any[] | undefined = undefined; - protected toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; + public toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; protected logger: Logger; constructor(options: MCPServerStreamableHttpOptions) { @@ -97,10 +92,7 @@ export abstract class BaseMCPServerStreamableHttp implements MCPServer { abstract get name(): string; abstract connect(): Promise; abstract close(): Promise; - abstract listTools( - runContext?: RunContext, - agent?: Agent, - ): Promise; + abstract listTools(): Promise; abstract callTool( _toolName: string, _args: Record | null, @@ -154,14 +146,11 @@ export class MCPServerStdio extends BaseMCPServerStdio { close(): Promise { return this.underlying.close(); } - async listTools( - runContext?: RunContext, - agent?: Agent, - ): Promise { + async listTools(): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(runContext, agent); + const tools = await this.underlying.listTools(); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -190,14 +179,11 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { close(): Promise { return this.underlying.close(); } - async listTools( - runContext?: RunContext, - agent?: Agent, - ): Promise { + async listTools(): Promise { if (this.cacheToolsList && this._cachedTools) { return this._cachedTools; } - const tools = await this.underlying.listTools(runContext, agent); + const tools = await this.underlying.listTools(); if (this.cacheToolsList) { this._cachedTools = tools; } @@ -217,18 +203,18 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { */ export async function getAllMcpFunctionTools( mcpServers: MCPServer[], + runContext: RunContext, + agent: Agent, convertSchemasToStrict = false, - runContext?: RunContext, - agent?: Agent, ): Promise[]> { const allTools: Tool[] = []; const toolNames = new Set(); for (const server of mcpServers) { const serverTools = await getFunctionToolsFromServer( server, - convertSchemasToStrict, runContext, agent, + convertSchemasToStrict, ); const serverToolNames = new Set(serverTools.map((t) => t.name)); const intersection = [...serverToolNames].filter((n) => toolNames.has(n)); @@ -259,16 +245,62 @@ export function invalidateServerToolsCache(serverName: string) { */ async function getFunctionToolsFromServer( server: MCPServer, + runContext: RunContext, + agent: Agent, convertSchemasToStrict: boolean, - runContext?: RunContext, - agent?: Agent, ): Promise[]> { if (server.cacheToolsList && _cachedTools[server.name]) { return _cachedTools[server.name]; } return withMCPListToolsSpan( async (span) => { - const mcpTools = await server.listTools(runContext, agent); + const fetchedMcpTools = await server.listTools(); + const mcpTools: MCPTool[] = []; + const context = { + runContext, + agent, + serverName: server.name, + }; + for (const tool of fetchedMcpTools) { + const filter = server.toolFilter; + if (filter) { + if (filter && typeof filter === 'function') { + const filtered = await filter(context, tool); + if (!filtered) { + globalLogger.debug( + `MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the callable filter.`, + ); + continue; // skip this tool + } + } else { + const allowedToolNames = filter.allowedToolNames ?? []; + const blockedToolNames = filter.blockedToolNames ?? []; + if (allowedToolNames.length > 0 || blockedToolNames.length > 0) { + const allowed = + allowedToolNames.length > 0 + ? allowedToolNames.includes(tool.name) + : true; + const blocked = + blockedToolNames.length > 0 + ? blockedToolNames.includes(tool.name) + : false; + if (!allowed || blocked) { + if (blocked) { + globalLogger.debug( + `MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the static filter.`, + ); + } else if (!allowed) { + globalLogger.debug( + `MCP Tool (server: ${server.name}, tool: ${tool.name}) is not allowed by the static filter.`, + ); + } + continue; // skip this tool + } + } + } + } + mcpTools.push(tool); + } span.spanData.result = mcpTools.map((t) => t.name); const tools: FunctionTool[] = mcpTools.map((t) => mcpToFunctionTool(t, server, convertSchemasToStrict), @@ -287,15 +319,15 @@ async function getFunctionToolsFromServer( */ export async function getAllMcpTools( mcpServers: MCPServer[], + runContext: RunContext, + agent: Agent, convertSchemasToStrict = false, - runContext?: RunContext, - agent?: Agent, ): Promise[]> { return getAllMcpFunctionTools( mcpServers, - convertSchemasToStrict, runContext, agent, + convertSchemasToStrict, ); } diff --git a/packages/agents-core/src/mcpUtil.ts b/packages/agents-core/src/mcpUtil.ts index 5d90ab10..54daa688 100644 --- a/packages/agents-core/src/mcpUtil.ts +++ b/packages/agents-core/src/mcpUtil.ts @@ -17,7 +17,7 @@ export interface MCPToolFilterContext { export type MCPToolFilterCallable = ( context: MCPToolFilterContext, tool: MCPTool, -) => boolean | Promise; +) => Promise; /** Static tool filter configuration using allow and block lists. */ export interface MCPToolFilterStatic { diff --git a/packages/agents-core/src/runState.ts b/packages/agents-core/src/runState.ts index 54abbd5e..a18e6493 100644 --- a/packages/agents-core/src/runState.ts +++ b/packages/agents-core/src/runState.ts @@ -557,8 +557,8 @@ export class RunState> { ? await deserializeProcessedResponse( agentMap, state._currentAgent, - stateJson.lastProcessedResponse, state._context, + stateJson.lastProcessedResponse, ) : undefined; @@ -708,12 +708,12 @@ export function deserializeItem( async function deserializeProcessedResponse( agentMap: Map>, currentAgent: Agent, + context: RunContext, serializedProcessedResponse: z.infer< typeof serializedProcessedResponseSchema >, - runContext: RunContext, ): Promise> { - const allTools = await currentAgent.getAllTools(runContext); + const allTools = await currentAgent.getAllTools(context); const tools = new Map( allTools .filter((tool) => tool.type === 'function') diff --git a/packages/agents-core/src/shims/mcp-server/browser.ts b/packages/agents-core/src/shims/mcp-server/browser.ts index 712b0808..7d9e7fbc 100644 --- a/packages/agents-core/src/shims/mcp-server/browser.ts +++ b/packages/agents-core/src/shims/mcp-server/browser.ts @@ -6,8 +6,6 @@ import { MCPServerStreamableHttpOptions, MCPTool, } from '../../mcp'; -import type { RunContext } from '../../runContext'; -import type { Agent } from '../../agent'; export class MCPServerStdio extends BaseMCPServerStdio { constructor(params: MCPServerStdioOptions) { @@ -22,10 +20,7 @@ export class MCPServerStdio extends BaseMCPServerStdio { close(): Promise { throw new Error('Method not implemented.'); } - listTools( - _runContext?: RunContext, - _agent?: Agent, - ): Promise { + listTools(): Promise { throw new Error('Method not implemented.'); } callTool( @@ -49,10 +44,7 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { close(): Promise { throw new Error('Method not implemented.'); } - listTools( - _runContext?: RunContext, - _agent?: Agent, - ): Promise { + listTools(): Promise { throw new Error('Method not implemented.'); } callTool( diff --git a/packages/agents-core/src/shims/mcp-server/node.ts b/packages/agents-core/src/shims/mcp-server/node.ts index 7a7825a7..ca600c23 100644 --- a/packages/agents-core/src/shims/mcp-server/node.ts +++ b/packages/agents-core/src/shims/mcp-server/node.ts @@ -12,8 +12,6 @@ import { invalidateServerToolsCache, } from '../../mcp'; import logger from '../../logger'; -import type { RunContext } from '../../runContext'; -import type { Agent } from '../../agent'; export interface SessionMessage { message: any; @@ -98,10 +96,7 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio { this._cacheDirty = true; } - async listTools( - _runContext?: RunContext, - _agent?: Agent, - ): Promise { + async listTools(): Promise { const { ListToolsResultSchema } = await import( '@modelcontextprotocol/sdk/types.js' ).catch(failedToImport); @@ -218,10 +213,7 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp { this._cacheDirty = true; } - async listTools( - _runContext?: RunContext, - _agent?: Agent, - ): Promise { + async listTools(): Promise { const { ListToolsResultSchema } = await import( '@modelcontextprotocol/sdk/types.js' ).catch(failedToImport); diff --git a/packages/agents-core/test/mcpCache.test.ts b/packages/agents-core/test/mcpCache.test.ts index dc763ab1..99d989cd 100644 --- a/packages/agents-core/test/mcpCache.test.ts +++ b/packages/agents-core/test/mcpCache.test.ts @@ -3,6 +3,8 @@ import { getAllMcpTools } from '../src/mcp'; import { withTrace } from '../src/tracing'; import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; import type { CallToolResultContent } from '../src/mcp'; +import { RunContext } from '../src/runContext'; +import { Agent } from '../src/agent'; class StubServer extends NodeMCPServerStdio { public toolList: any[]; @@ -48,15 +50,27 @@ describe('MCP tools cache invalidation', () => { ]; const server = new StubServer('server', toolsA); - let tools = await getAllMcpTools([server]); + let tools = await getAllMcpTools( + [server], + new RunContext({}), + new Agent({ name: 'test' }), + ); expect(tools.map((t) => t.name)).toEqual(['a']); server.toolList = toolsB; - tools = await getAllMcpTools([server]); + tools = await getAllMcpTools( + [server], + new RunContext({}), + new Agent({ name: 'test' }), + ); expect(tools.map((t) => t.name)).toEqual(['a']); server.invalidateToolsCache(); - tools = await getAllMcpTools([server]); + tools = await getAllMcpTools( + [server], + new RunContext({}), + new Agent({ name: 'test' }), + ); expect(tools.map((t) => t.name)).toEqual(['b']); }); }); diff --git a/packages/agents-core/test/mcpToolFilter.test.ts b/packages/agents-core/test/mcpToolFilter.test.ts index 23a0e7de..7b8bb867 100644 --- a/packages/agents-core/test/mcpToolFilter.test.ts +++ b/packages/agents-core/test/mcpToolFilter.test.ts @@ -2,8 +2,6 @@ import { describe, it, expect } from 'vitest'; import { withTrace } from '../src/tracing'; import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; import { createMCPToolStaticFilter } from '../src/mcpUtil'; -import { Agent } from '../src/agent'; -import { RunContext } from '../src/runContext'; class StubServer extends NodeMCPServerStdio { public toolList: any[]; @@ -51,16 +49,7 @@ describe('MCP tool filtering', () => { tools, createMCPToolStaticFilter({ allowed: ['a'], blocked: ['b'] }), ); - const agent = new Agent({ - name: 'agent', - instructions: '', - model: '', - modelSettings: {}, - tools: [], - mcpServers: [], - }); - const runContext = new RunContext(); - const result = await server.listTools(runContext, agent); + const result = await server.listTools(); expect(result.map((t) => t.name)).toEqual(['a', 'b']); }); }); @@ -91,16 +80,7 @@ describe('MCP tool filtering', () => { ]; const filter = (_ctx: any, tool: any) => tool.name !== 'bad'; const server = new StubServer('s', tools, filter); - const agent = new Agent({ - name: 'agent', - instructions: '', - model: '', - modelSettings: {}, - tools: [], - mcpServers: [], - }); - const runContext = new RunContext(); - const result = await server.listTools(runContext, agent); + const result = await server.listTools(); expect(result.map((t) => t.name)).toEqual(['good', 'bad']); }); }); @@ -147,17 +127,8 @@ describe('MCP tool filtering', () => { createMCPToolStaticFilter({ allowed: ['a1'] }), ); const serverB = new StubServer('B', toolsB); - const agent = new Agent({ - name: 'agent', - instructions: '', - model: '', - modelSettings: {}, - tools: [], - mcpServers: [], - }); - const runContext = new RunContext(); - const resultA = await serverA.listTools(runContext, agent); - const resultB = await serverB.listTools(runContext, agent); + const resultA = await serverA.listTools(); + const resultB = await serverB.listTools(); expect(resultA.map((t) => t.name)).toEqual(['a1', 'a2']); expect(resultB.map((t) => t.name)).toEqual(['b1']); }); @@ -182,21 +153,12 @@ describe('MCP tool filtering', () => { tools, createMCPToolStaticFilter({ allowed: ['x'] }), ); - const agent = new Agent({ - name: 'agent', - instructions: '', - model: '', - modelSettings: {}, - tools: [], - mcpServers: [], - }); - const runContext = new RunContext(); - let result = await server.listTools(runContext, agent); + let result = await server.listTools(); expect(result.map((t) => t.name)).toEqual(['x']); (server as any).toolFilter = createMCPToolStaticFilter({ allowed: ['y'], }); - result = await server.listTools(runContext, agent); + result = await server.listTools(); expect(result.map((t) => t.name)).toEqual(['x']); }); }); From e4ba592ff2b3ffc1d044cf87bbb9cabd4af9ad65 Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:37:24 -0700 Subject: [PATCH 13/14] chore(example): update tool-filter example after seratch 73eb37a merge --- examples/mcp/tool-filter-example.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/mcp/tool-filter-example.ts b/examples/mcp/tool-filter-example.ts index 5e5c8d6a..d0764815 100644 --- a/examples/mcp/tool-filter-example.ts +++ b/examples/mcp/tool-filter-example.ts @@ -29,7 +29,10 @@ async function main() { }); console.log('Listing sample files:'); - let result = await run(agent, 'List the files in the current directory.'); + let result = await run( + agent, + 'List the files in the sample_files directory.', + ); console.log(result.finalOutput); console.log('\nAttempting to write a file (should be blocked):'); From b73a0907518bcfead994928e45a5d158363c7865 Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:20:30 -0700 Subject: [PATCH 14/14] test(agents-core): fix mcpCache tests by passing RunContext and Agent to getAllMcpTools --- packages/agents-core/test/mcpCache.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/agents-core/test/mcpCache.test.ts b/packages/agents-core/test/mcpCache.test.ts index 2a6452ee..961eb04d 100644 --- a/packages/agents-core/test/mcpCache.test.ts +++ b/packages/agents-core/test/mcpCache.test.ts @@ -87,7 +87,11 @@ describe('MCP tools cache invalidation', () => { ]; const serverA = new StubServer('server', tools); - await getAllMcpTools([serverA]); + await getAllMcpTools( + [serverA], + new RunContext({}), + new Agent({ name: 'test' }), + ); const serverB = new StubServer('server', tools); let called = false; @@ -96,7 +100,11 @@ describe('MCP tools cache invalidation', () => { return []; }; - const cachedTools = (await getAllMcpTools([serverB])) as FunctionTool[]; + const cachedTools = (await getAllMcpTools( + [serverB], + new RunContext({}), + new Agent({ name: 'test' }), + )) as FunctionTool[]; await cachedTools[0].invoke({} as any, '{}'); expect(called).toBe(true);