From d94ee86bd3d4191f9574ccbe8fb77040eae52ba6 Mon Sep 17 00:00:00 2001 From: xierenhong Date: Tue, 5 Aug 2025 22:23:32 +0800 Subject: [PATCH 1/6] refactor: restructure mcp tools fetching with options object pattern --- packages/agents-core/src/agent.ts | 7 +- packages/agents-core/src/index.ts | 2 + packages/agents-core/src/mcp.ts | 178 +++++++++++++++--------------- 3 files changed, 100 insertions(+), 87 deletions(-) diff --git a/packages/agents-core/src/agent.ts b/packages/agents-core/src/agent.ts index 114ec884..d40dd610 100644 --- a/packages/agents-core/src/agent.ts +++ b/packages/agents-core/src/agent.ts @@ -518,7 +518,12 @@ export class Agent< runContext: RunContext, ): Promise[]> { if (this.mcpServers.length > 0) { - return getAllMcpTools(this.mcpServers, runContext, this, false); + return getAllMcpTools({ + mcpServers: this.mcpServers, + runContext, + agent: this, + convertSchemasToStrict: false, + }); } return []; diff --git a/packages/agents-core/src/index.ts b/packages/agents-core/src/index.ts index a5b9f9d3..6f3fd125 100644 --- a/packages/agents-core/src/index.ts +++ b/packages/agents-core/src/index.ts @@ -70,10 +70,12 @@ export { getLogger } from './logger'; export { getAllMcpTools, invalidateServerToolsCache, + mcpToFunctionTool, MCPServer, MCPServerStdio, MCPServerStreamableHttp, MCPServerSSE, + GetAllMcpToolsOptions, } from './mcp'; export { MCPToolFilterCallable, diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index aa993ea6..31248466 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -285,35 +285,6 @@ export class MCPServerSSE extends BaseMCPServerSSE { * Fetches and flattens all tools from multiple MCP servers. * Logs and skips any servers that fail to respond. */ -export async function getAllMcpFunctionTools( - mcpServers: MCPServer[], - runContext: RunContext, - agent: Agent, - convertSchemasToStrict = false, -): Promise[]> { - const allTools: Tool[] = []; - const toolNames = new Set(); - for (const server of mcpServers) { - const serverTools = await getFunctionToolsFromServer( - server, - runContext, - agent, - convertSchemasToStrict, - ); - const serverToolNames = new Set(serverTools.map((t) => t.name)); - const intersection = [...serverToolNames].filter((n) => toolNames.has(n)); - if (intersection.length > 0) { - throw new UserError( - `Duplicate tool names found across MCP servers: ${intersection.join(', ')}`, - ); - } - for (const t of serverTools) { - toolNames.add(t.name); - allTools.push(t); - } - } - return allTools; -} const _cachedTools: Record = {}; /** @@ -327,12 +298,17 @@ export async function invalidateServerToolsCache(serverName: string) { /** * Fetches all function tools from a single MCP server. */ -async function getFunctionToolsFromServer( - server: MCPServer, - runContext: RunContext, - agent: Agent, - convertSchemasToStrict: boolean, -): Promise[]> { +async function getFunctionToolsFromServer({ + server, + convertSchemasToStrict, + runContext, + agent, +}: { + server: MCPServer; + convertSchemasToStrict: boolean; + runContext?: RunContext; + agent?: Agent; +}): Promise[]> { if (server.cacheToolsList && _cachedTools[server.name]) { return _cachedTools[server.name].map((t) => mcpToFunctionTool(t, server, convertSchemasToStrict), @@ -341,52 +317,54 @@ async function getFunctionToolsFromServer( return withMCPListToolsSpan( async (span) => { 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.`, - ); + let mcpTools: MCPTool[] = fetchedMcpTools; + + if (runContext && agent) { + const context = { runContext, agent, serverName: server.name }; + const filteredTools: MCPTool[] = []; + for (const tool of fetchedMcpTools) { + const filter = server.toolFilter; + if (filter) { + if (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; + } + } 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; } - continue; // skip this tool } } } + filteredTools.push(tool); } - mcpTools.push(tool); + mcpTools = filteredTools; } + span.spanData.result = mcpTools.map((t) => t.name); const tools: FunctionTool[] = mcpTools.map((t) => mcpToFunctionTool(t, server, convertSchemasToStrict), @@ -400,21 +378,49 @@ async function getFunctionToolsFromServer( ); } +/** + * Options for fetching MCP tools. + */ +export type GetAllMcpToolsOptions = { + mcpServers: MCPServer[]; + convertSchemasToStrict?: boolean; + runContext?: RunContext; + agent?: Agent; +}; + /** * Returns all MCP tools from the provided servers, using the function tool conversion. + * If runContext and agent are provided, callable tool filters will be applied. */ -export async function getAllMcpTools( - mcpServers: MCPServer[], - runContext: RunContext, - agent: Agent, +export async function getAllMcpTools({ + mcpServers, convertSchemasToStrict = false, -): Promise[]> { - return getAllMcpFunctionTools( - mcpServers, - runContext, - agent, - convertSchemasToStrict, - ); + runContext, + agent, +}: GetAllMcpToolsOptions): Promise[]> { + const allTools: Tool[] = []; + const toolNames = new Set(); + + for (const server of mcpServers) { + 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)); + if (intersection.length > 0) { + throw new UserError( + `Duplicate tool names found across MCP servers: ${intersection.join(', ')}`, + ); + } + for (const t of serverTools) { + toolNames.add(t.name); + allTools.push(t); + } + } + return allTools; } /** From 0171e90aab937aa32b1ecf48dde4f0dd84e2f8b3 Mon Sep 17 00:00:00 2001 From: xierenhong Date: Tue, 5 Aug 2025 22:55:51 +0800 Subject: [PATCH 2/6] chore: add changeset md --- .changeset/seven-rice-shop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/seven-rice-shop.md diff --git a/.changeset/seven-rice-shop.md b/.changeset/seven-rice-shop.md new file mode 100644 index 00000000..2e8dbe6f --- /dev/null +++ b/.changeset/seven-rice-shop.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +refactor: restructure mcp tools fetching with options object pattern From b35feff68e174757de929e6d0c613584dd953c49 Mon Sep 17 00:00:00 2001 From: xierenhong Date: Wed, 6 Aug 2025 10:24:49 +0800 Subject: [PATCH 3/6] feat: add getAllMcpTools function with example and overloads [AI] --- examples/mcp/README.md | 6 ++ examples/mcp/get-all-mcp-tools-example.ts | 120 ++++++++++++++++++++++ examples/mcp/package.json | 7 +- packages/agents-core/src/mcp.ts | 37 +++++-- 4 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 examples/mcp/get-all-mcp-tools-example.ts diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 858ed2a3..14c14bbe 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -18,3 +18,9 @@ pnpm -F mcp start:stdio ```bash pnpm -F mcp start:tool-filter ``` + +`get-all-mcp-tools-example.ts` demonstrates how to use the `getAllMcpTools` function to fetch tools from multiple MCP servers: + +```bash +pnpm -F mcp start:get-all-tools +``` diff --git a/examples/mcp/get-all-mcp-tools-example.ts b/examples/mcp/get-all-mcp-tools-example.ts new file mode 100644 index 00000000..6bb76e8c --- /dev/null +++ b/examples/mcp/get-all-mcp-tools-example.ts @@ -0,0 +1,120 @@ +import { + Agent, + run, + MCPServerStdio, + getAllMcpTools, + withTrace, +} from '@openai/agents'; +import * as path from 'node:path'; +import { createOpenAI } from '@ai-sdk/openai'; +import { aisdk } from '@openai/agents-extensions'; + +const openai = createOpenAI({ + baseURL: 'https://api.moonshot.cn/v1', + apiKey: process.env.MOONSHOT_API_KEY, +}); + +const model = aisdk(openai('kimi-k2-turbo-preview')); + +async function main() { + const samplesDir = path.join(__dirname, 'sample_files'); + + // Create multiple MCP servers to demonstrate getAllMcpTools + const filesystemServer = new MCPServerStdio({ + name: 'Filesystem Server', + fullCommand: `npx -y @modelcontextprotocol/server-filesystem ${samplesDir}`, + }); + + // Note: This example shows how to use multiple servers + // In practice, you would have different servers with different tools + const servers = [filesystemServer]; + + // Connect all servers + for (const server of servers) { + await server.connect(); + } + + try { + await withTrace('getAllMcpTools Example', async () => { + console.log('=== Using getAllMcpTools to fetch all tools ===\n'); + + // Method 1: Simple array of servers + const allTools = await getAllMcpTools(servers); + console.log( + `Found ${allTools.length} tools from ${servers.length} server(s):`, + ); + allTools.forEach((tool) => { + const description = + tool.type === 'function' ? tool.description : 'No description'; + console.log(`- ${tool.name}: ${description}`); + }); + + console.log('\n=== Using getAllMcpTools with options object ===\n'); + + // Method 2: Using options object (recommended for more control) + const allToolsWithOptions = await getAllMcpTools({ + mcpServers: servers, + convertSchemasToStrict: true, // Convert schemas to strict mode + }); + + console.log( + `Found ${allToolsWithOptions.length} tools with strict schemas:`, + ); + allToolsWithOptions.forEach((tool) => { + const description = + tool.type === 'function' ? tool.description : 'No description'; + console.log(`- ${tool.name}: ${description}`); + }); + + console.log('\n=== Creating agent with pre-fetched tools ===\n'); + + // Create agent using the pre-fetched tools + const agent = new Agent({ + name: 'MCP Assistant with Pre-fetched Tools', + instructions: + 'Use the available tools to help the user with file operations.', + tools: allTools, // Use pre-fetched tools instead of mcpServers + model, + }); + + // Test the agent + const message = 'List the available files and read one of them.'; + console.log(`Running: ${message}\n`); + const result = await run(agent, message); + console.log(result.finalOutput); + + console.log( + '\n=== Demonstrating tool filtering with getAllMcpTools ===\n', + ); + + // Add tool filter to one of the servers + filesystemServer.toolFilter = { + allowedToolNames: ['read_file'], // Only allow read_file tool + }; + + // Note: For callable filters to work, you need to pass runContext and agent + // This is typically done internally when the agent runs + const filteredTools = await getAllMcpTools({ + mcpServers: servers, + convertSchemasToStrict: false, + // runContext and agent would normally be provided by the agent runtime + // For demo purposes, we're showing the structure + }); + + console.log(`After filtering, found ${filteredTools.length} tools:`); + filteredTools.forEach((tool) => { + console.log(`- ${tool.name}`); + }); + }); + } finally { + // Clean up - close all servers + for (const server of servers) { + await server.close(); + } + } +} + +main().catch((err) => { + console.error('Error:', err); + process.exit(1); +}); diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 3e0c255c..4f0775ae 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -4,7 +4,9 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "@openai/agents": "workspace:*", - "zod": "3.25.40 - 3.25.67" + "zod": "3.25.40 - 3.25.67", + "@openai/agents-extensions": "workspace:*", + "@ai-sdk/openai": "^1.1.3" }, "scripts": { "build-check": "tsc --noEmit", @@ -14,6 +16,7 @@ "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:tool-filter": "tsx tool-filter-example.ts", - "start:sse": "tsx sse-example.ts" + "start:sse": "tsx sse-example.ts", + "start:get-all-tools": "tsx get-all-mcp-tools-example.ts" } } diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index 31248466..f517bc6d 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -392,21 +392,42 @@ export type GetAllMcpToolsOptions = { * Returns all MCP tools from the provided servers, using the function tool conversion. * If runContext and agent are provided, callable tool filters will be applied. */ -export async function getAllMcpTools({ - mcpServers, +export async function getAllMcpTools( + mcpServers: MCPServer[], +): Promise[]>; +export async function getAllMcpTools( + opts: GetAllMcpToolsOptions, +): Promise[]>; +export async function getAllMcpTools( + mcpServersOrOpts: MCPServer[] | GetAllMcpToolsOptions, + runContext?: RunContext, + agent?: Agent, convertSchemasToStrict = false, - runContext, - agent, -}: GetAllMcpToolsOptions): Promise[]> { +): Promise[]> { + const opts = Array.isArray(mcpServersOrOpts) + ? { + mcpServers: mcpServersOrOpts, + runContext, + agent, + convertSchemasToStrict, + } + : mcpServersOrOpts; + + const { + mcpServers, + convertSchemasToStrict: convertSchemasToStrictFromOpts = false, + runContext: runContextFromOpts, + agent: agentFromOpts, + } = opts; const allTools: Tool[] = []; const toolNames = new Set(); for (const server of mcpServers) { const serverTools = await getFunctionToolsFromServer({ server, - convertSchemasToStrict, - runContext, - agent, + convertSchemasToStrict: convertSchemasToStrictFromOpts, + runContext: runContextFromOpts, + agent: agentFromOpts, }); const serverToolNames = new Set(serverTools.map((t) => t.name)); const intersection = [...serverToolNames].filter((n) => toolNames.has(n)); From e9669bc0fbbcf77b48bc7d9ea0365d8c44f3b8cd Mon Sep 17 00:00:00 2001 From: xierenhong Date: Wed, 6 Aug 2025 10:27:20 +0800 Subject: [PATCH 4/6] refactor: remove unused openai imports and model configuration --- examples/mcp/get-all-mcp-tools-example.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/examples/mcp/get-all-mcp-tools-example.ts b/examples/mcp/get-all-mcp-tools-example.ts index 6bb76e8c..419c718e 100644 --- a/examples/mcp/get-all-mcp-tools-example.ts +++ b/examples/mcp/get-all-mcp-tools-example.ts @@ -6,15 +6,6 @@ import { withTrace, } from '@openai/agents'; import * as path from 'node:path'; -import { createOpenAI } from '@ai-sdk/openai'; -import { aisdk } from '@openai/agents-extensions'; - -const openai = createOpenAI({ - baseURL: 'https://api.moonshot.cn/v1', - apiKey: process.env.MOONSHOT_API_KEY, -}); - -const model = aisdk(openai('kimi-k2-turbo-preview')); async function main() { const samplesDir = path.join(__dirname, 'sample_files'); @@ -74,7 +65,6 @@ async function main() { instructions: 'Use the available tools to help the user with file operations.', tools: allTools, // Use pre-fetched tools instead of mcpServers - model, }); // Test the agent From d4c24cc41eab07a6aa37206fc3774c23bf8e6c0f Mon Sep 17 00:00:00 2001 From: xierenhong Date: Wed, 6 Aug 2025 10:29:38 +0800 Subject: [PATCH 5/6] chore: remove unused dependencies from mcp example --- examples/mcp/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 4f0775ae..a65ed18e 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -4,9 +4,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "@openai/agents": "workspace:*", - "zod": "3.25.40 - 3.25.67", - "@openai/agents-extensions": "workspace:*", - "@ai-sdk/openai": "^1.1.3" + "zod": "3.25.40 - 3.25.67" }, "scripts": { "build-check": "tsc --noEmit", From f6db6fa6ffd9f5e69f5dfee3b482080884bdb307 Mon Sep 17 00:00:00 2001 From: xierenhong Date: Mon, 11 Aug 2025 15:26:32 +0800 Subject: [PATCH 6/6] refactor: update getAllMcpTools to use named parameters --- packages/agents-core/test/mcpCache.test.ts | 50 +++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/agents-core/test/mcpCache.test.ts b/packages/agents-core/test/mcpCache.test.ts index 537b9983..42c83eb5 100644 --- a/packages/agents-core/test/mcpCache.test.ts +++ b/packages/agents-core/test/mcpCache.test.ts @@ -51,27 +51,27 @@ describe('MCP tools cache invalidation', () => { ]; const server = new StubServer('server', toolsA); - let tools = await getAllMcpTools( - [server], - new RunContext({}), - new Agent({ name: 'test' }), - ); + let tools = await getAllMcpTools({ + mcpServers: [server], + runContext: new RunContext({}), + agent: new Agent({ name: 'test' }), + }); expect(tools.map((t) => t.name)).toEqual(['a']); server.toolList = toolsB; - tools = await getAllMcpTools( - [server], - new RunContext({}), - new Agent({ name: 'test' }), - ); + tools = await getAllMcpTools({ + mcpServers: [server], + runContext: new RunContext({}), + agent: new Agent({ name: 'test' }), + }); expect(tools.map((t) => t.name)).toEqual(['a']); await server.invalidateToolsCache(); - tools = await getAllMcpTools( - [server], - new RunContext({}), - new Agent({ name: 'test' }), - ); + tools = await getAllMcpTools({ + mcpServers: [server], + runContext: new RunContext({}), + agent: new Agent({ name: 'test' }), + }); expect(tools.map((t) => t.name)).toEqual(['b']); }); }); @@ -87,11 +87,11 @@ describe('MCP tools cache invalidation', () => { ]; const serverA = new StubServer('server', tools); - await getAllMcpTools( - [serverA], - new RunContext({}), - new Agent({ name: 'test' }), - ); + await getAllMcpTools({ + mcpServers: [serverA], + runContext: new RunContext({}), + agent: new Agent({ name: 'test' }), + }); const serverB = new StubServer('server', tools); let called = false; @@ -100,11 +100,11 @@ describe('MCP tools cache invalidation', () => { return []; }; - const cachedTools = (await getAllMcpTools( - [serverB], - new RunContext({}), - new Agent({ name: 'test' }), - )) as FunctionTool[]; + const cachedTools = (await getAllMcpTools({ + mcpServers: [serverB], + runContext: new RunContext({}), + agent: new Agent({ name: 'test' }), + })) as FunctionTool[]; await cachedTools[0].invoke({} as any, '{}'); expect(called).toBe(true);