From 48ebb1cdc3b5f33439bae9b752ae1e5d84fb3274 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/17] 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 f598775e..8e639ff2 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 365a8fc9..cea0377b 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 e36bec9c..e8be25b0 100644 --- a/packages/agents-core/src/index.ts +++ b/packages/agents-core/src/index.ts @@ -74,6 +74,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 7c7f03d2..fad7a20a 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, @@ -41,18 +47,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, @@ -74,6 +85,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) { @@ -81,12 +93,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, @@ -141,11 +157,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; } @@ -177,11 +196,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; } @@ -205,6 +227,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(); @@ -212,6 +236,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)); @@ -243,6 +269,8 @@ export async 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].map((t) => @@ -251,7 +279,7 @@ async function getFunctionToolsFromServer( } 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), @@ -271,8 +299,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, + ); } /** @@ -363,6 +398,7 @@ export interface BaseMCPServerStdioOptions { encoding?: string; encodingErrorHandler?: 'strict' | 'ignore' | 'replace'; logger?: Logger; + toolFilter?: ToolFilterCallable | ToolFilterStatic; } export interface DefaultMCPServerStdioOptions extends BaseMCPServerStdioOptions { @@ -383,6 +419,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 c86d7501..982624a3 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 d6e4c23a..60b0f962 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( @@ -47,7 +52,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 11000d78..f34e7620 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 ebb4cb3b..f8f7ea5f 100644 --- a/packages/agents-realtime/src/realtimeSession.ts +++ b/packages/agents-realtime/src/realtimeSession.ts @@ -276,7 +276,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, @@ -446,7 +446,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 39f74dda33d7e071382c30a600eb3574cd8853a1 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/17] 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 f8f7ea5f..3c4f22ef 100644 --- a/packages/agents-realtime/src/realtimeSession.ts +++ b/packages/agents-realtime/src/realtimeSession.ts @@ -275,10 +275,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, ]; } @@ -445,12 +446,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 41612538e932d82acdf8493117cc2fea3f916960 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/17] 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 0e7694b412f43712b7ffe8228a2db946d3c676f1 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/17] 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 74cc833eac2e43d6b2133aedcb1c1fa02ccfce78 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/17] 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 14fbfc17a3e6e2e2898fc68f0c3b768c954211d7 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/17] 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 8e639ff2..6de6d1b1 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 2ddaea8e2472537d2e838a98799de17d0c2e7f7b 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/17] 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 f34e7620..d33a1862 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 66b6c6b4486f1f229a64993ea8ba8b99d570e123 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/17] 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 fad7a20a..6679d488 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; @@ -47,7 +52,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) { @@ -85,7 +90,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) { @@ -158,13 +163,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; } @@ -197,13 +202,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; } @@ -279,7 +284,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), @@ -383,6 +397,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 4e5c53d8eadb182a2e67c525a9ad4d745b932d35 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/17] 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 6679d488..fad7a20a 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; @@ -52,7 +47,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) { @@ -90,7 +85,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) { @@ -163,13 +158,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; } @@ -202,13 +197,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; } @@ -284,16 +279,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), @@ -397,41 +383,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 d7ab6fd79b7e7b89b2c8368d8ee066c4b9c726ea 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/17] 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 6de6d1b1..de121e54 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 e8be25b0..85f1474e 100644 --- a/packages/agents-core/src/index.ts +++ b/packages/agents-core/src/index.ts @@ -75,10 +75,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 fad7a20a..c45efb17 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'; @@ -47,7 +47,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) { @@ -85,7 +85,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) { @@ -398,7 +398,7 @@ export interface BaseMCPServerStdioOptions { encoding?: string; encodingErrorHandler?: 'strict' | 'ignore' | 'replace'; logger?: Logger; - toolFilter?: ToolFilterCallable | ToolFilterStatic; + toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic; } export interface DefaultMCPServerStdioOptions extends BaseMCPServerStdioOptions { @@ -419,7 +419,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 eeaa067a46dc346448b00cee260c81ba61112c2d Mon Sep 17 00:00:00 2001 From: gilbertl Date: Wed, 16 Jul 2025 01:50:01 +0800 Subject: [PATCH 11/17] 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 +++++ 1 file changed, 5 insertions(+) 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 From 04297fa95c9fe26e34903d58f727013cba7b7914 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 16 Jul 2025 15:53:16 +0900 Subject: [PATCH 12/17] 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 cea0377b..114ec884 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 c45efb17..bbfa7dab 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, @@ -47,7 +45,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) { @@ -60,10 +58,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, @@ -85,7 +80,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) { @@ -99,10 +94,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, @@ -157,14 +149,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; } @@ -196,14 +185,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; } @@ -226,18 +212,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)); @@ -268,9 +254,9 @@ export async 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].map((t) => @@ -279,7 +265,53 @@ async function getFunctionToolsFromServer( } 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), @@ -298,15 +330,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 60b0f962..d6e4c23a 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( @@ -52,10 +47,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 d33a1862..0ae306c6 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 0f5f7428..02042cbf 100644 --- a/packages/agents-core/test/mcpCache.test.ts +++ b/packages/agents-core/test/mcpCache.test.ts @@ -4,6 +4,8 @@ import type { FunctionTool } from '../src/tool'; 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[]; @@ -49,15 +51,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']); await 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 d37724fb2d7d14cba66abb3eb6ecb54867e81ccf 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/17] 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 957fc8bd0302c684807518c169e905c5be41b3a4 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/17] 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 02042cbf..537b9983 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); From 803d9e2c17166f85e52e0c05b8675a9eddbdf4fd Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:53:15 -0700 Subject: [PATCH 15/17] test: expand unit tests and add MCP filter integration test --- package.json | 1 + .../test/mcpToolFilter.integration.test.ts | 151 +++++++++++ .../agents-core/test/mcpToolFilter.test.ts | 244 ++++++++++++++++++ 3 files changed, 396 insertions(+) create mode 100644 packages/agents-core/test/mcpToolFilter.integration.test.ts diff --git a/package.json b/package.json index f814222e..dc4dbcd7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "examples:tools-computer-use": "pnpm -F tools start:computer-use", "examples:tools-file-search": "pnpm -F tools start:file-search", "examples:tools-web-search": "pnpm -F tools start:web-search", + "examples:tool-filter": "tsx examples/mcp/tool-filter-example.ts", "ci:publish": "pnpm publish -r --no-git-checks", "bump-version": "changeset version && pnpm -F @openai/* prebuild", "prepare": "husky", diff --git a/packages/agents-core/test/mcpToolFilter.integration.test.ts b/packages/agents-core/test/mcpToolFilter.integration.test.ts new file mode 100644 index 00000000..d1d9ae9a --- /dev/null +++ b/packages/agents-core/test/mcpToolFilter.integration.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { Agent, run, setDefaultModelProvider } from '../src'; +import { mcpToFunctionTool } from '../src/mcp'; +import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; +import { createMCPToolStaticFilter } from '../src/mcpUtil'; +import { FakeModel, FakeModelProvider } from './stubs'; +import { Usage } from '../src/usage'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +class StubFilesystemServer extends NodeMCPServerStdio { + private dir: string; + public tools: any[]; + constructor(dir: string, filter: any) { + super({ command: 'noop', name: 'stubfs', cacheToolsList: true }); + this.dir = dir; + this.toolFilter = filter; + this.tools = [ + { + name: 'read_file', + description: '', + inputSchema: { + type: 'object', + properties: { path: { type: 'string' } }, + required: ['path'], + additionalProperties: false, + }, + }, + { + name: 'list_directory', + description: '', + inputSchema: { + type: 'object', + properties: { path: { type: 'string' } }, + required: ['path'], + additionalProperties: false, + }, + }, + { + name: 'write_file', + description: '', + inputSchema: { + type: 'object', + properties: { + path: { type: 'string' }, + text: { type: 'string' }, + }, + required: ['path', 'text'], + additionalProperties: false, + }, + }, + ]; + } + async connect() {} + async close() {} + async listTools() { + return this.tools; + } + async callTool(name: string, args: any) { + const blocked = (this.toolFilter as any)?.blockedToolNames ?? []; + if (blocked.includes(name)) { + return [ + { type: 'text', text: `Tool "${name}" is blocked by MCP filter` }, + ]; + } + if (name === 'list_directory') { + const files = fs.readdirSync(this.dir); + return [{ type: 'text', text: files.join('\n') }]; + } + if (name === 'read_file') { + const text = fs.readFileSync(path.join(this.dir, args.path), 'utf8'); + return [{ type: 'text', text }]; + } + if (name === 'write_file') { + fs.writeFileSync(path.join(this.dir, args.path), args.text, 'utf8'); + return [{ type: 'text', text: 'ok' }]; + } + return []; + } +} + +describe('MCP tool filter integration', () => { + beforeAll(() => { + setDefaultModelProvider(new FakeModelProvider()); + }); + const samplesDir = path.join(__dirname, '../../../examples/mcp/sample_files'); + const filter = createMCPToolStaticFilter({ + allowed: ['read_file', 'list_directory', 'write_file'], + blocked: ['write_file'], + }); + const server = new StubFilesystemServer(samplesDir, filter); + const tools = server.tools.map((t) => mcpToFunctionTool(t, server, false)); + + it('allows listing files', async () => { + const modelResponses = [ + { + output: [ + { + id: '1', + type: 'function_call', + name: 'list_directory', + callId: '1', + status: 'completed', + arguments: '{}', + }, + ], + usage: new Usage(), + }, + ]; + const agent = new Agent({ + name: 'Lister', + toolUseBehavior: 'stop_on_first_tool', + model: new FakeModel(modelResponses), + tools, + }); + const result = await run(agent, 'List files'); + expect(result.finalOutput).toContain('books.txt'); + expect(result.finalOutput).toContain('favorite_songs.txt'); + }); + + it('blocks write_file', async () => { + const modelResponses = [ + { + output: [ + { + id: '1', + type: 'function_call', + name: 'write_file', + callId: '1', + status: 'completed', + arguments: '{"path":"test.txt","text":"hello"}', + }, + ], + usage: new Usage(), + }, + ]; + const agent = new Agent({ + name: 'Writer', + toolUseBehavior: 'stop_on_first_tool', + model: new FakeModel(modelResponses), + tools, + }); + const result = await run(agent, 'write'); + expect(result.finalOutput).toBe( + JSON.stringify({ + type: 'text', + text: 'Tool "write_file" is blocked by MCP filter', + }), + ); + }); +}); diff --git a/packages/agents-core/test/mcpToolFilter.test.ts b/packages/agents-core/test/mcpToolFilter.test.ts index 7b8bb867..a36c2c60 100644 --- a/packages/agents-core/test/mcpToolFilter.test.ts +++ b/packages/agents-core/test/mcpToolFilter.test.ts @@ -162,4 +162,248 @@ describe('MCP tool filtering', () => { expect(result.map((t) => t.name)).toEqual(['x']); }); }); + + it('returns all tools when no filter is provided', async () => { + await withTrace('test', async () => { + const tools = [ + { + name: 'x', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'y', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + + const server = new StubServer('nofilter', tools); + const result = await server.listTools(); + expect(result.map((t) => t.name)).toEqual(['x', 'y']); + }); + }); + + it('blocks only the tools in blocked list when no allowed list is given', 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, + }, + }, + { + name: 'c', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + + const server = new StubServer( + 'blockonly', + tools, + createMCPToolStaticFilter({ blocked: ['b', 'c'] }), + ); + + const result = await server.listTools(); + + expect(result.map((t) => t.name)).toEqual(['a', 'b', 'c']); + }); + }); + + it('allows only the tools in allowed list when no blocked list is given', 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, + }, + }, + { + name: 'c', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + + const server = new StubServer( + 'allowonly', + tools, + createMCPToolStaticFilter({ allowed: ['b'] }), + ); + + const result = await server.listTools(); + // The shim still returns the raw list; actual filtering is applied later + expect(result.map((t) => t.name)).toEqual(['a', 'b', 'c']); + }); + }); + + it('supports async filter functions', async () => { + await withTrace('test', async () => { + const tools = [ + { + name: 'one', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'two', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + + const asyncFilter = async (_ctx: any, tool: any) => { + await new Promise((r) => setTimeout(r, 10)); + return tool.name === 'one'; + }; + + const server = new StubServer('async', tools, asyncFilter); + const result = await server.listTools(); + // listTools itself returns the raw list; filtering happens later + expect(result.map((t) => t.name)).toEqual(['one', 'two']); + }); + }); + + it('recovers when filter function throws', async () => { + await withTrace('test', async () => { + const tools = [ + { + name: 'x', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'y', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + + const badFilter = () => { + throw new Error('boom'); + }; + const server = new StubServer('explode', tools, badFilter); + + // Just await the call — if it throws, the test will fail. + const result = await server.listTools(); + expect(result.map((t) => t.name)).toEqual(['x', 'y']); + }); + }); + + it('reloads tools after invalidation', async () => { + await withTrace('test', async () => { + const tools1 = [ + { + name: 'a', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + const tools2 = [ + { + name: 'b', + description: '', + inputSchema: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + ]; + + const server = new StubServer( + 'reloader', + tools1, + createMCPToolStaticFilter({ allowed: ['a'] }), + ); + + // first call sees tools1 + let result = await server.listTools(); + expect(result.map((t) => t.name)).toEqual(['a']); + + // swap out the underlying list, invalidate cache, then call again + server.toolList = tools2 as any; + server.invalidateToolsCache(); + + result = await server.listTools(); + expect(result.map((t) => t.name)).toEqual(['b']); + }); + }); }); From 26a27463cd95f5e7c89002230ad33390cce5de8e Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:12:36 -0700 Subject: [PATCH 16/17] test: fix modelResponses typing for FakeModel in MCP integration test --- packages/agents-core/test/mcpToolFilter.integration.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/agents-core/test/mcpToolFilter.integration.test.ts b/packages/agents-core/test/mcpToolFilter.integration.test.ts index d1d9ae9a..dfa37a2c 100644 --- a/packages/agents-core/test/mcpToolFilter.integration.test.ts +++ b/packages/agents-core/test/mcpToolFilter.integration.test.ts @@ -5,6 +5,7 @@ import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; import { createMCPToolStaticFilter } from '../src/mcpUtil'; import { FakeModel, FakeModelProvider } from './stubs'; import { Usage } from '../src/usage'; +import type { ModelResponse } from '../src'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -92,7 +93,7 @@ describe('MCP tool filter integration', () => { const tools = server.tools.map((t) => mcpToFunctionTool(t, server, false)); it('allows listing files', async () => { - const modelResponses = [ + const modelResponses: ModelResponse[] = [ { output: [ { @@ -119,7 +120,7 @@ describe('MCP tool filter integration', () => { }); it('blocks write_file', async () => { - const modelResponses = [ + const modelResponses: ModelResponse[] = [ { output: [ { From 161965f494d8bd449b9020f9beb4c875a04b6eb2 Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Mon, 28 Jul 2025 00:21:46 -0700 Subject: [PATCH 17/17] docs: update MCP guide, add tool-filter example, and remove unrelated changeset --- .changeset/heavy-yaks-mate.md | 5 ----- docs/src/content/docs/guides/mcp.mdx | 26 ++++++++------------------ examples/docs/mcp/tool-filter.ts | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 23 deletions(-) delete mode 100644 .changeset/heavy-yaks-mate.md create mode 100644 examples/docs/mcp/tool-filter.ts diff --git a/.changeset/heavy-yaks-mate.md b/.changeset/heavy-yaks-mate.md deleted file mode 100644 index 3094f31d..00000000 --- a/.changeset/heavy-yaks-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@openai/agents-openai": patch ---- - -Handle function call messages with empty content in Chat Completions diff --git a/docs/src/content/docs/guides/mcp.mdx b/docs/src/content/docs/guides/mcp.mdx index de121e54..3b59230e 100644 --- a/docs/src/content/docs/guides/mcp.mdx +++ b/docs/src/content/docs/guides/mcp.mdx @@ -10,6 +10,7 @@ import hostedStreamExample from '../../../../../examples/docs/mcp/hostedStream.t import hostedHITLExample from '../../../../../examples/docs/mcp/hostedHITL.ts?raw'; import streamableHttpExample from '../../../../../examples/docs/mcp/streamableHttp.ts?raw'; import stdioExample from '../../../../../examples/docs/mcp/stdio.ts?raw'; +import toolFilterExample from '../../../../../examples/docs/mcp/tool-filter.ts?raw'; The [**Model Context Protocol (MCP)**](https://modelcontextprotocol.io) is an open protocol that standardizes how applications provide tools and context to LLMs. From the MCP docs: @@ -99,24 +100,13 @@ 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 `createMCPToolStaticFilter` or a custom function: - -```ts -const server = new MCPServerStdio({ - fullCommand: 'my-server', - 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', -}); -``` +You can restrict which tools are exposed from each server by passing either a static filter via `createMCPToolStaticFilter` or a custom function. Here’s a combined example showing both approaches: + + ## Further reading diff --git a/examples/docs/mcp/tool-filter.ts b/examples/docs/mcp/tool-filter.ts new file mode 100644 index 00000000..8a1cea76 --- /dev/null +++ b/examples/docs/mcp/tool-filter.ts @@ -0,0 +1,24 @@ +import { + MCPServerStdio, + MCPServerStreamableHttp, + createMCPToolStaticFilter, + MCPToolFilterContext, +} from '@openai/agents'; + +interface ToolFilterContext { + allowAll: boolean; +} + +const server = new MCPServerStdio({ + fullCommand: 'my-server', + toolFilter: createMCPToolStaticFilter({ + allowed: ['safe_tool'], + blocked: ['danger_tool'], + }), +}); + +const dynamicServer = new MCPServerStreamableHttp({ + url: 'http://localhost:3000', + toolFilter: async ({ runContext }: MCPToolFilterContext, tool) => + (runContext.context as ToolFilterContext).allowAll || tool.name !== 'admin', +}); \ No newline at end of file